atris 3.30.8 → 3.31.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 (59) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +4 -2
  4. package/atris/atris.md +51 -19
  5. package/atris/skills/README.md +1 -0
  6. package/atris/skills/blocks/SKILL.md +134 -0
  7. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  8. package/atris/skills/youtube/SKILL.md +31 -11
  9. package/atris.md +7 -0
  10. package/ax +367 -93
  11. package/bin/atris.js +317 -155
  12. package/commands/autoland.js +319 -0
  13. package/commands/autopilot.js +94 -4
  14. package/commands/business.js +1 -1
  15. package/commands/clean.js +72 -9
  16. package/commands/codex-goal.js +72 -22
  17. package/commands/computer.js +48 -3
  18. package/commands/gm.js +1 -1
  19. package/commands/harvest.js +179 -0
  20. package/commands/init.js +1 -1
  21. package/commands/land.js +442 -0
  22. package/commands/loop-front.js +122 -4
  23. package/commands/member.js +519 -19
  24. package/commands/mission.js +3659 -282
  25. package/commands/play.js +1 -1
  26. package/commands/pulse.js +65 -7
  27. package/commands/run.js +3 -3
  28. package/commands/strings.js +301 -0
  29. package/commands/task.js +575 -102
  30. package/commands/truth.js +170 -0
  31. package/commands/xp.js +32 -8
  32. package/commands/youtube.js +72 -5
  33. package/decks/README.md +6 -12
  34. package/lib/auto-accept-certified.js +10 -0
  35. package/lib/autoland.js +283 -0
  36. package/lib/context-gatherer.js +0 -8
  37. package/lib/mission-artifact.js +504 -0
  38. package/lib/mission-room.js +846 -0
  39. package/lib/mission-runtime-loop.js +320 -0
  40. package/lib/next-moves.js +212 -6
  41. package/lib/pulse.js +74 -1
  42. package/lib/runner-command.js +20 -8
  43. package/lib/runs-prune.js +242 -0
  44. package/lib/task-proof.js +1 -1
  45. package/package.json +3 -3
  46. package/decks/atris-seed-pitch-v3.json +0 -118
  47. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  48. package/decks/atris-seed-pitch-v5.json +0 -109
  49. package/decks/atris-seed-pitch-v6.json +0 -137
  50. package/decks/atris-seed-pitch-v7.json +0 -133
  51. package/decks/mark-pincus-narrative.json +0 -102
  52. package/decks/mark-pincus-sourcery.json +0 -94
  53. package/decks/yash-applied-compute-detailed.json +0 -150
  54. package/decks/yash-applied-compute-generalist.json +0 -82
  55. package/decks/yash-applied-compute-narrative.json +0 -54
  56. package/lib/ax-chat-input.js +0 -164
  57. package/lib/ax-goal.js +0 -307
  58. package/lib/ax-prefs.js +0 -63
  59. package/lib/ax-shimmer.js +0 -63
@@ -15,11 +15,27 @@ const {
15
15
  normalizeOwnerSlug,
16
16
  resolveFunctionalOwner,
17
17
  } = require('../lib/functional-owner');
18
+ const {
19
+ buildMissionRoom,
20
+ writeMissionRoomReceipt,
21
+ missionRoomLines,
22
+ } = require('../lib/mission-room');
23
+ const {
24
+ missionArtifactPaths,
25
+ writeMissionArtifact,
26
+ } = require('../lib/mission-artifact');
27
+ const {
28
+ pruneRuns,
29
+ runsPruneLines,
30
+ formatBytes,
31
+ } = require('../lib/runs-prune');
18
32
 
19
33
  const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
20
34
  const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
21
35
  const GOAL_LOOP_STATUSES = new Set(['planning', 'running', 'ready']);
22
36
  const STATUS_ALIASES = new Set(['active']);
37
+ const DEFAULT_LONG_RUN_VERIFIER = 'git diff --check';
38
+ const SLEEP_LENGTH_BUDGET_SECONDS = 3600;
23
39
 
24
40
  function stampIso() {
25
41
  return new Date().toISOString();
@@ -84,6 +100,16 @@ function readPositiveIntegerFlag(args, name, fallback = null, options = {}) {
84
100
  return value;
85
101
  }
86
102
 
103
+ function readNonNegativeIntegerFlag(args, name, fallback = null) {
104
+ const raw = readFlag(args, name, '');
105
+ if (!raw) return fallback;
106
+ const value = Number(raw);
107
+ if (!Number.isInteger(value) || value < 0) {
108
+ throw new Error(`${name} must be a non-negative integer.`);
109
+ }
110
+ return value;
111
+ }
112
+
87
113
  function readRepeatedFlag(args, name) {
88
114
  const values = [];
89
115
  const prefix = `${name}=`;
@@ -147,6 +173,8 @@ function printJsonOrText(payload, lines, asJson) {
147
173
  const MISSION_RUN_VALUE_FLAGS = [
148
174
  '--max-ticks',
149
175
  '--max-wall',
176
+ '--minutes',
177
+ '--hours',
150
178
  '--cadence',
151
179
  '--owner',
152
180
  '--runner',
@@ -154,6 +182,10 @@ const MISSION_RUN_VALUE_FLAGS = [
154
182
  '--verify',
155
183
  '--stop',
156
184
  '--model',
185
+ '--native-goal-status',
186
+ '--native-goal-objective',
187
+ '--visible-goal-status',
188
+ '--visible-goal-objective',
157
189
  ];
158
190
  const MISSION_RUN_BOOLEAN_FLAGS = [
159
191
  '--json',
@@ -162,6 +194,16 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
162
194
  '--no-verify',
163
195
  '--complete-on-pass',
164
196
  '--no-drain',
197
+ '--create-next',
198
+ '--spend-full-budget',
199
+ '--use-whole-budget',
200
+ '--stop-when-done',
201
+ '--room-preflight',
202
+ '--no-room-preflight',
203
+ '--room-auto-run',
204
+ '--no-room-auto-run',
205
+ '--allow-native-goal-supersede',
206
+ '--supersede-paused-native-goal',
165
207
  '--always-on',
166
208
  '--xp-task',
167
209
  '--agent-xp',
@@ -321,7 +363,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
321
363
  taskDb.noteTask(db, {
322
364
  id: task.id,
323
365
  actor: process.env.ATRIS_AGENT_ID || owner,
324
- content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP lands only after human accept.`,
366
+ content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP is awarded only after human approval.`,
325
367
  });
326
368
  }
327
369
  const { outPath } = writeMissionTaskProjection(taskDb, db, workspaceRoot);
@@ -345,7 +387,10 @@ function statePaths(root = process.cwd()) {
345
387
  missionsJsonl: path.join(stateDir, 'missions.jsonl'),
346
388
  eventsJsonl: path.join(stateDir, 'mission_events.jsonl'),
347
389
  codexGoalJson: path.join(stateDir, 'codex_goal.json'),
390
+ codexGoalRequestJson: path.join(stateDir, 'codex_goal_request.json'),
348
391
  codexGoalStatus: path.join(root, 'atris', 'status', 'codex-goal.md'),
392
+ atrisGoalJson: path.join(stateDir, 'atris_goal.json'),
393
+ atrisGoalStatus: path.join(root, 'atris', 'status', 'atris-goal.md'),
349
394
  statusNow: path.join(root, 'atris', 'status', 'now.md'),
350
395
  runsDir: path.join(root, 'atris', 'runs'),
351
396
  };
@@ -402,9 +447,21 @@ function terminalNextAction(status) {
402
447
 
403
448
  function normalizeMissionState(mission) {
404
449
  if (!mission) return mission;
450
+ let normalized = mission;
405
451
  const nextAction = terminalNextAction(mission.status);
406
- if (!nextAction || mission.next_action === nextAction) return mission;
407
- return { ...mission, next_action: nextAction };
452
+ if (nextAction && mission.next_action !== nextAction) {
453
+ normalized = { ...normalized, next_action: nextAction };
454
+ }
455
+ const effectiveVerifier = effectiveMissionVerifier(normalized);
456
+ const explicitVerifier = String(normalized.verifier || '').trim();
457
+ if (effectiveVerifier && effectiveVerifier !== explicitVerifier) {
458
+ return { ...normalized, effective_verifier: effectiveVerifier };
459
+ }
460
+ if (Object.prototype.hasOwnProperty.call(normalized, 'effective_verifier')) {
461
+ const { effective_verifier, ...withoutDerivedVerifier } = normalized;
462
+ return withoutDerivedVerifier;
463
+ }
464
+ return normalized;
408
465
  }
409
466
 
410
467
  function listMissions(root = process.cwd()) {
@@ -616,6 +673,7 @@ function completionGateLabel(gate) {
616
673
  function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
617
674
  const gate = mission.completion_gate || {};
618
675
  const gateLabel = completionGateLabel(gate) || 'completion gate';
676
+ const happened = `${mission.objective} is complete.`;
619
677
  const checked = gate.source === 'receipt' && gate.receipt_path
620
678
  ? `I checked the passing verifier receipt ${gate.receipt_path}.`
621
679
  : gate.source === 'mission_state'
@@ -624,21 +682,23 @@ function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
624
682
  ? 'No verifier was configured, so completion used the no-verifier gate.'
625
683
  : `I checked the ${gateLabel} completion gate.`;
626
684
  const tested = mission.verifier_result?.passed
627
- ? `Verifier passed: ${mission.verifier_result.command || mission.verifier || 'configured verifier'}.`
685
+ ? missionVerifierHighLevelTestText(mission.verifier_result, mission)
628
686
  : mission.verifier
629
687
  ? `Completion proof is attached for verifier: ${mission.verifier}.`
630
688
  : 'No verifier command was recorded for this mission.';
631
689
  const landing = {
632
- happened: `Mission completed: ${mission.objective}.`,
690
+ happened,
691
+ reason: missionHumanReasonText(mission, happened),
633
692
  checked,
634
693
  tested,
635
- saved: `Saved complete mission ${mission.id}${proof ? ` with proof ${proof}` : ''}.`,
694
+ saved: proof ? `Proof saved at ${proof}.` : 'Proof saved in mission state.',
636
695
  decision: xpNextCommand
637
- ? `Queue AgentXP next: ${xpNextCommand}`
638
- : 'Mission is complete; start or resume the next mission if more work remains.',
696
+ ? 'Ready for human review; accept in Atris if the proof looks right.'
697
+ : 'Pick the next customer-facing move.',
639
698
  };
640
699
  const result = {
641
700
  changed: landing.happened,
701
+ reason: landing.reason,
642
702
  checked: landing.checked,
643
703
  tested: landing.tested,
644
704
  saved: landing.saved,
@@ -650,12 +710,657 @@ function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
650
710
  function missionResultLines(completion) {
651
711
  const landing = completion?.landing || {};
652
712
  const result = completion?.result || {};
653
- const lines = ['Result:'];
654
- if (landing.happened) lines.push(` What happened: ${landing.happened}`);
713
+ const lines = ['Landing:'];
714
+ if (landing.happened) lines.push(` Changed: ${landing.happened}`);
715
+ if (landing.reason) lines.push(` Why it matters: ${landing.reason}`);
716
+ if (landing.artifact) lines.push(` Artifact: ${landing.artifact}`);
655
717
  if (landing.checked) lines.push(` How I checked: ${landing.checked}`);
656
718
  if (landing.tested) lines.push(` What I tested: ${landing.tested}`);
657
- if (result.saved) lines.push(` Saved: ${result.saved}`);
658
- if (landing.decision) lines.push(` Decision: ${landing.decision}`);
719
+ if (result.saved) lines.push(` Proof: ${result.saved}`);
720
+ if (landing.decision) lines.push(` Next: ${landing.decision}`);
721
+ return lines;
722
+ }
723
+
724
+ function missionSelfImprovementSeedAction(mission, root = process.cwd()) {
725
+ try {
726
+ const moves = require('../lib/next-moves');
727
+ const seed = moves.nextMoves(root, 3).find((move) => (
728
+ move
729
+ && move.source === 'mission'
730
+ && (!mission?.id || !move.ref || move.ref === mission.id)
731
+ && move.title !== mission.objective
732
+ ));
733
+ return seed ? `${seed.title}.` : '';
734
+ } catch {
735
+ return '';
736
+ }
737
+ }
738
+
739
+ function missionHumanNextAction(mission, root = process.cwd(), options = {}) {
740
+ if (!mission) return 'Pick the next customer-facing move.';
741
+ if (mission.goal_chain?.pause_ready) return 'Mission feels good; review proof, then complete, revise, or choose the next goal.';
742
+ if (mission.status === 'ready' && /^queue AgentXP review:/i.test(mission.next_action || '')) {
743
+ return 'Ready for human review; accept in Atris if the proof looks right.';
744
+ }
745
+ if (mission.status === 'ready' && mission.always_on) {
746
+ return options.allowSelfImprovementSeed
747
+ ? (missionSelfImprovementSeedAction(mission, root) || 'Run the next proof step.')
748
+ : 'Run the next proof step.';
749
+ }
750
+ if (mission.status === 'ready') return 'Review the proof, then complete the mission.';
751
+ if (mission.status === 'complete') return 'Pick the next customer-facing move.';
752
+ if (mission.status === 'blocked') return 'Fix the verifier failure or revise the mission.';
753
+ return 'Keep running the mission.';
754
+ }
755
+
756
+ function missionLandingStepSummary(summary) {
757
+ const clean = String(summary || '').replace(/\s+/g, ' ').trim();
758
+ if (!clean) return '';
759
+ const withoutLabel = clean
760
+ .replace(/^(?:landing|changed|summary|result|product proof|proof):\s*/i, '')
761
+ .trim();
762
+ if (!withoutLabel) return '';
763
+ const plainVerified = missionPlainVerifiedSummary(withoutLabel);
764
+ if (plainVerified) return plainVerified;
765
+ const beforeChecks = withoutLabel.split(/\s+(?:checks?|verified|proof):\s+/i)[0] || withoutLabel;
766
+ const clipped = missionLandingSentenceClip(beforeChecks, 220);
767
+ return clipped ? `${clipped}.` : '';
768
+ }
769
+
770
+ function missionHumanReasonText(mission, changed = '') {
771
+ const objective = String(mission?.objective || '').trim();
772
+ const text = `${objective} ${changed}`.toLowerCase();
773
+ if (/\b(human|plain|language|landing|proof|receipt|understand|readable)\b/.test(text)) {
774
+ return 'It makes the result understandable before a human accepts or rejects it.';
775
+ }
776
+ if (/\b(update|install|runner|autopilot|mission run|heartbeat)\b/.test(text)) {
777
+ return 'It proves the workflow works in the place people actually use it.';
778
+ }
779
+ return 'It turns the mission into a concrete result a human can accept, reject, or run again.';
780
+ }
781
+
782
+ function missionPlainVerifiedSummary(text) {
783
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
784
+ const shipped = clean.match(/^Verified\s+(.+?)\s+behavior\s+already\s+shipped\s+on\s+master\s*:/i);
785
+ if (!shipped) return '';
786
+ const topic = shipped[1].toLowerCase();
787
+ if (topic.includes('npm auto-update')) {
788
+ return 'Verified npm auto-update works for installed npm packages on master.';
789
+ }
790
+ if (topic.includes('runner-agnostic heartbeat')) {
791
+ return 'Verified autopilot and mission run use the same runner setup on master.';
792
+ }
793
+ return `Verified ${shipped[1]} is already working on master.`;
794
+ }
795
+
796
+ function missionLandingSentenceClip(text, max) {
797
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
798
+ if (!clean) return '';
799
+ if (clean.length <= max) return clean.replace(/[.!?:;,]+$/g, '').trim();
800
+ const sentence = clean.match(/^(.{24,220}?[.!?])\s+/);
801
+ const base = sentence ? sentence[1] : clean.slice(0, max);
802
+ return base
803
+ .replace(/\s+\S*$/, '')
804
+ .replace(/[.!?:;,]+$/g, '')
805
+ .replace(/\b(?:and|or|but|with|to|for|from|by|through|via|using|including|include|includes|plus|then|both|the|a|an|of|in|on|at|as)$/i, '')
806
+ .trim();
807
+ }
808
+
809
+ function missionLastStepSummary(ticks = []) {
810
+ for (let index = ticks.length - 1; index >= 0; index -= 1) {
811
+ const tick = ticks[index] || {};
812
+ const summary = missionLandingStepSummary(tick.summary || tick.claude?.summary || tick.atris2?.receipt_text || '');
813
+ if (summary) return summary;
814
+ }
815
+ return '';
816
+ }
817
+
818
+ function missionVerifierCheckedText(verifierResult, mission) {
819
+ if (!verifierResult) return 'Tick recorded; no verifier was run.';
820
+ const command = verifierResult.command || mission.verifier || 'configured verifier';
821
+ if (verifierResult.passed) {
822
+ if (/^git\s+diff\s+--check\b/i.test(command)) return 'I ran the diff cleanliness check.';
823
+ if (/\bnode\s+--test\b/i.test(command)) return 'I ran the behavior checks.';
824
+ return `Verifier passed: ${command}.`;
825
+ }
826
+ if (/^git\s+diff\s+--check\b/i.test(command)) return 'Diff cleanliness check failed.';
827
+ if (/\bnode\s+--test\b/i.test(command)) return 'Behavior checks failed.';
828
+ return `Verifier failed: ${command}.`;
829
+ }
830
+
831
+ function missionVerifierHighLevelTestText(verifierResult, mission) {
832
+ if (!verifierResult) return 'No automated verifier ran for this receipt; judge it from the receipt, changed files, and next action.';
833
+ const command = verifierResult.command || mission.verifier || 'configured verifier';
834
+ const outcome = verifierResult.passed ? 'passed' : 'failed';
835
+ if (/^git\s+diff\s+--check\b/i.test(command)) {
836
+ return `Diff cleanliness check ${outcome}: no whitespace or patch-format issues in the changed files.`;
837
+ }
838
+ if (/\bnode\s+--test\b/i.test(command) && /\btest\/mission-status\.test\.js\b/i.test(command)) {
839
+ return `Mission behavior checks ${outcome}: mission start, tick, completion, timeline landing, goal-chain, next-mission, and human-accept boundaries were exercised.`;
840
+ }
841
+ if (/\bnode\s+--test\b/i.test(command)) {
842
+ return `Automated behavior checks ${outcome}: the touched code path was exercised by Node tests.`;
843
+ }
844
+ return `Verifier command ${outcome}: ${command}.`;
845
+ }
846
+
847
+ function missionFallbackChangedText(mission, status, tickIndex, { ranTicks = null, effectiveMaxTicks = null } = {}) {
848
+ if (mission?.always_on && (status === 'ready' || status === 'running')) {
849
+ return 'Recorded a proof heartbeat for this always-on mission.';
850
+ }
851
+ if (status === 'ready') return `${mission.objective} is ready for review.`;
852
+ if (status === 'complete') return `${mission.objective} is complete.`;
853
+ if (status === 'blocked') return `${mission.objective} is blocked.`;
854
+ if (ranTicks != null && effectiveMaxTicks != null) {
855
+ return ranTicks > 0
856
+ ? `${mission.objective} ran ${ranTicks}/${effectiveMaxTicks} tick(s).`
857
+ : `${mission.objective} did not run a tick.`;
858
+ }
859
+ return `${mission.objective} recorded tick ${tickIndex}.`;
860
+ }
861
+
862
+ function missionTickResultLines(mission, tickIndex, receiptPath, verifierResult = null, stepSummary = '') {
863
+ return missionLandingLines(missionReceiptLanding(mission, {
864
+ tick: { tick_index: tickIndex },
865
+ summary: stepSummary,
866
+ verifier_result: verifierResult,
867
+ }, receiptPath));
868
+ }
869
+
870
+ function missionReceiptSummaryText(result) {
871
+ const tick = result?.tick || {};
872
+ return tick.summary
873
+ || tick.atris2?.receipt_text
874
+ || tick.claude?.receipt_text
875
+ || result?.summary
876
+ || result?.reason
877
+ || '';
878
+ }
879
+
880
+ function missionReceiptTickIndex(mission, result) {
881
+ const value = Number(result?.tick?.tick_index || mission?.last_tick_index || 0);
882
+ return Number.isFinite(value) && value > 0 ? value : 1;
883
+ }
884
+
885
+ function missionReceiptStatus(mission, result) {
886
+ const tickStatus = String(result?.tick?.status || '').trim();
887
+ if (tickStatus === 'blocked') return 'blocked';
888
+ if (result?.verifier_result?.passed === false) return 'blocked';
889
+ if (result?.verifier_result?.passed === true) return 'proof_ready';
890
+ return String(mission?.status || 'running');
891
+ }
892
+
893
+ function missionReceiptNextText(mission, result) {
894
+ if (result?.next) return String(result.next);
895
+ if (result?.landing?.next) return String(result.landing.next);
896
+ if (result?.verifier_result?.passed === true) {
897
+ return mission?.always_on ? 'Run the next proof step.' : 'Review the proof, then complete the mission.';
898
+ }
899
+ if (result?.verifier_result) return 'Fix the verifier failure or revise the mission.';
900
+ return missionHumanNextAction(mission);
901
+ }
902
+
903
+ function missionReceiptLanding(mission, result, receiptPath = '') {
904
+ const verifierResult = result?.verifier_result || null;
905
+ const status = missionReceiptStatus(mission, result);
906
+ const summary = missionReceiptSummaryText(result);
907
+ const changed = missionLandingStepSummary(summary)
908
+ || missionFallbackChangedText(mission, status, missionReceiptTickIndex(mission, result));
909
+ const checked = missionVerifierCheckedText(verifierResult, mission);
910
+ const tested = missionVerifierHighLevelTestText(verifierResult, mission);
911
+ const reason = missionHumanReasonText(mission, changed);
912
+ return {
913
+ schema: 'atris.result_landing.v1',
914
+ status,
915
+ changed,
916
+ reason,
917
+ checked,
918
+ tested,
919
+ proof: receiptPath ? `Receipt saved at ${receiptPath}.` : 'Receipt saved in mission run history.',
920
+ next: missionReceiptNextText(mission, result),
921
+ timeline_visible: !missionLandingChangedIsGenericTick(mission, changed),
922
+ };
923
+ }
924
+
925
+ function missionLandingLines(landing) {
926
+ if (!landing) return [];
927
+ return [
928
+ 'Landing:',
929
+ ` Changed: ${landing.changed || landing.happened || 'Landing recorded.'}`,
930
+ ` Why it matters: ${landing.reason || landing.why || 'This makes the work easier to judge.'}`,
931
+ ` How I checked: ${landing.checked || 'No check recorded.'}`,
932
+ ` What I tested: ${landing.tested || 'No test summary recorded.'}`,
933
+ ` Proof: ${landing.proof || landing.saved || 'No proof path recorded.'}`,
934
+ ` Next: ${landing.next || landing.decision || 'Pick the next useful move.'}`,
935
+ ];
936
+ }
937
+
938
+ function missionStatusLandingLines(landing) {
939
+ const lines = missionLandingLines(landing);
940
+ if (!lines.length) return [];
941
+ return [
942
+ ' last landing:',
943
+ ...lines.slice(1).map((line) => ` ${line.trim()}`),
944
+ ];
945
+ }
946
+
947
+ function missionLastLanding(mission, root = process.cwd()) {
948
+ const receipt = readMissionReceipt(mission?.receipt_path, mission?.worktree_root || root);
949
+ const landing = receipt?.result?.landing;
950
+ if (!landing || typeof landing !== 'object') return null;
951
+ return {
952
+ ...landing,
953
+ receipt_path: mission.receipt_path || null,
954
+ };
955
+ }
956
+
957
+ function missionLandingChangedIsGenericTick(mission, changed) {
958
+ const text = String(changed || '').trim();
959
+ const objective = String(mission?.objective || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
960
+ if (!objective) return false;
961
+ return new RegExp(`^${objective} recorded tick \\d+\\.$`).test(text);
962
+ }
963
+
964
+ function normalizeMissionReceiptResult(mission, result, receiptPath = '') {
965
+ const object = result && typeof result === 'object' && !Array.isArray(result)
966
+ ? { ...result }
967
+ : { value: result };
968
+ if (!object.landing) {
969
+ object.landing = missionReceiptLanding(mission, object, receiptPath);
970
+ }
971
+ if (object.verifier_result && !('passed' in object)) {
972
+ object.passed = !!object.verifier_result.passed;
973
+ }
974
+ return object;
975
+ }
976
+
977
+ function missionRunStartNextLine(mission, nextCommand, warnings = []) {
978
+ if (warnings.length) return 'Add a verifier before completion, then run the first proof tick.';
979
+ if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
980
+ return 'Start the visible goal, then continue this mission.';
981
+ }
982
+ if (/attach-task/.test(nextCommand || '')) return 'Attach task context, then continue this mission.';
983
+ return 'Run the first proof tick.';
984
+ }
985
+
986
+ function missionRunTakeoffLines(mission, { warnings = [], nextCommand = '' } = {}) {
987
+ const checked = mission.verifier
988
+ ? `Verifier configured: ${mission.verifier}.`
989
+ : 'No verifier was recorded for this mission.';
990
+ return [
991
+ 'Takeoff:',
992
+ ` Goal: ${mission.objective}`,
993
+ ` Done when: ${mission.stop_condition || 'the mission has proof or a human decision'}.`,
994
+ ...(missionBudgetLine(mission) ? [` Budget: ${missionBudgetLine(mission)}`] : []),
995
+ ...missionGoalChainLines(mission),
996
+ ' Proof: Mission state saved in .atris/state/missions.jsonl.',
997
+ ` Check: ${checked}`,
998
+ ` Next: ${missionRunStartNextLine(mission, nextCommand, warnings)}`,
999
+ ];
1000
+ }
1001
+
1002
+ function missionRunPreflightSignals(text) {
1003
+ const lower = String(text || '').toLowerCase().replace(/\s+/g, ' ');
1004
+ return /\b(messy|shower|overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve|figure\s+out|think\s+through|thinkwell|what\s+to\s+do\s+next|keep\s+going|tell\s+me|right\s+mission\s+input|finish[-\s]+line)\b/i.test(lower);
1005
+ }
1006
+
1007
+ function shouldMissionRunRoomPreflight(objective, args = []) {
1008
+ if (hasFlag(args, '--no-room-preflight')) return false;
1009
+ if (hasFlag(args, '--room-preflight')) return true;
1010
+ return missionRunPreflightSignals(objective) || missionRunTrustedRoomSignals(objective);
1011
+ }
1012
+
1013
+ function missionRunTrustedRoomSignals(text) {
1014
+ const lower = String(text || '').toLowerCase().replace(/\s+/g, ' ');
1015
+ return /\b(one[-\s]?message|autonomy|autonomous|self[-\s]?improve|improve\s+(atris|this|it)|keep\s+going|work\s+on\s+this|next\s+useful|goes?\s+off|no\s+junk|junk\s+state)\b/i.test(lower);
1016
+ }
1017
+
1018
+ function shouldMissionRunTrustedRoom(rawObjective, args = []) {
1019
+ if (hasFlag(args, '--no-room-auto-run')) return false;
1020
+ if (hasFlag(args, '--room-auto-run')) return true;
1021
+ return missionRunTrustedRoomSignals(rawObjective);
1022
+ }
1023
+
1024
+ function missionRunConcreteTitle(value) {
1025
+ const title = String(value || '').replace(/\s+/g, ' ').trim();
1026
+ if (!title) return '';
1027
+ if (/^(go|go go|go go go|keep going|do it|start|run|continue)$/i.test(title)) return '';
1028
+ if (title.length < 8) return '';
1029
+ return title;
1030
+ }
1031
+
1032
+ function selectMissionRunUsefulTarget(rawObjective, root = process.cwd()) {
1033
+ try {
1034
+ const moves = require('../lib/next-moves');
1035
+ const rawTitle = missionRunConcreteTitle(rawObjective);
1036
+ return moves.nextMoves(root, 8)
1037
+ .filter((move) => missionRunConcreteTitle(move?.title))
1038
+ .filter((move) => !rawTitle || String(move.title).trim() !== rawTitle)
1039
+ .find((move) => move.source === 'task')
1040
+ || moves.nextMoves(root, 8).find((move) => missionRunConcreteTitle(move?.title))
1041
+ || null;
1042
+ } catch {
1043
+ return null;
1044
+ }
1045
+ }
1046
+
1047
+ function missionRunPreflightSentence(text, max = 140) {
1048
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
1049
+ if (clean.length <= max) return clean;
1050
+ return `${clean.slice(0, max - 3).replace(/\s+\S*$/, '').trimEnd()}...`;
1051
+ }
1052
+
1053
+ function missionRunPreflightObjective(rawObjective, room, owner) {
1054
+ const name = room?.name || 'Mission Room';
1055
+ const task = room?.task_plan_preview?.task || room?.truth_snapshot || rawObjective;
1056
+ return `${name} with ${owner}: turn "${missionRunPreflightSentence(task)}" into one visible goal, task spine, proof receipt, and next action.`;
1057
+ }
1058
+
1059
+ function missionRunTrustedObjective(rawObjective, room, target) {
1060
+ const targetTitle = missionRunConcreteTitle(target?.title);
1061
+ if (targetTitle) return targetTitle;
1062
+ if (missionRunTrustedRoomSignals(rawObjective)) {
1063
+ return 'Improve Atris one-message autonomy without creating junk mission state';
1064
+ }
1065
+ return missionRunPreflightObjective(rawObjective, room, room?.owner || 'mission-lead');
1066
+ }
1067
+
1068
+ function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
1069
+ if (!shouldMissionRunRoomPreflight(rawObjective, args)) return null;
1070
+ const root = options.root || process.cwd();
1071
+ const owner = options.owner || readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
1072
+ const trustedRun = options.allowTrustedRun !== false && shouldMissionRunTrustedRoom(rawObjective, args);
1073
+ const selectedTarget = trustedRun ? selectMissionRunUsefulTarget(rawObjective, root) : null;
1074
+ const ownerResolution = resolveFunctionalOwner({
1075
+ requestedOwner: owner,
1076
+ title: selectedTarget?.title || rawObjective,
1077
+ tag: readFlag(args, '--lane', 'workspace'),
1078
+ goal: selectedTarget?.title || rawObjective,
1079
+ root,
1080
+ fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
1081
+ });
1082
+ const room = buildMissionRoom(rawObjective, {
1083
+ root,
1084
+ owner: ownerResolution.owner,
1085
+ ownerResolution,
1086
+ trustedRun,
1087
+ selectedTarget,
1088
+ verifier: trustedRun ? DEFAULT_LONG_RUN_VERIFIER : '',
1089
+ });
1090
+ const written = writeMissionRoomReceipt(room, { root });
1091
+ const shapedObjective = trustedRun
1092
+ ? missionRunTrustedObjective(rawObjective, written.room, selectedTarget)
1093
+ : missionRunPreflightObjective(rawObjective, written.room, ownerResolution.owner);
1094
+ return {
1095
+ schema: 'atris.mission_run_preflight.v1',
1096
+ source: 'mission_room',
1097
+ raw_objective: rawObjective,
1098
+ shaped_objective: shapedObjective,
1099
+ visible_goal_objective: shapedObjective,
1100
+ room_name: written.room.name,
1101
+ room_receipt_path: written.relativePath,
1102
+ owner: ownerResolution.owner,
1103
+ owner_resolution: ownerResolution.reason,
1104
+ trusted_run: trustedRun,
1105
+ selected_target: selectedTarget ? {
1106
+ title: selectedTarget.title,
1107
+ source: selectedTarget.source,
1108
+ task_id: selectedTarget.task_id || null,
1109
+ ref: selectedTarget.ref || null,
1110
+ why: selectedTarget.why || '',
1111
+ } : null,
1112
+ task_spine_required: !selectedTarget,
1113
+ next_action: selectedTarget
1114
+ ? 'run one proof tick for the selected existing task'
1115
+ : 'attach task spine, then run one proof tick',
1116
+ };
1117
+ }
1118
+
1119
+ function missionRunTaskLabel(task) {
1120
+ const ref = task?.ref || task?.display_id || task?.id || 'task';
1121
+ return [ref, task?.title].filter(Boolean).join(' ');
1122
+ }
1123
+
1124
+ function missionRunSelectedTaskTarget(preflight) {
1125
+ const target = preflight?.selected_target;
1126
+ if (!target || target.source !== 'task') return null;
1127
+ const taskId = String(target.task_id || '').trim();
1128
+ const ref = String(target.ref || taskId || '').trim();
1129
+ if (!taskId && !ref) return null;
1130
+ return {
1131
+ task_id: taskId || ref,
1132
+ task_ref: ref || taskId,
1133
+ title: target.title || '',
1134
+ };
1135
+ }
1136
+
1137
+ function attachSelectedTargetTaskSpine(mission) {
1138
+ const selected = missionRunSelectedTaskTarget(mission?.mission_run_preflight);
1139
+ if (!selected) return mission;
1140
+ const taskIds = Array.from(new Set([...(mission.task_ids || []), selected.task_id].filter(Boolean)));
1141
+ return {
1142
+ ...mission,
1143
+ task_ids: taskIds,
1144
+ current_task_id: selected.task_id,
1145
+ task_ref: selected.task_ref,
1146
+ task_scope_ref: selected.task_ref || selected.task_id,
1147
+ selected_target_task: selected,
1148
+ next_action: `work selected task then run: atris task current-step --goal-id ${selected.task_ref || selected.task_id} --as ${mission.owner} --proof "<proof>" --json`,
1149
+ };
1150
+ }
1151
+
1152
+ function missionRunCreatedNextChangedText(createdNext) {
1153
+ const createdTask = createdNext?.ok ? createdNext.task : null;
1154
+ if (createdTask) {
1155
+ return `Created and claimed next task: ${missionRunTaskLabel(createdTask)}.`;
1156
+ }
1157
+ const activeTask = createdNext?.reason === 'active_task' ? createdNext.move : null;
1158
+ if (activeTask) {
1159
+ return `Kept active task: ${missionRunTaskLabel(activeTask)}. No duplicate was created.`;
1160
+ }
1161
+ return null;
1162
+ }
1163
+
1164
+ function missionRunCreatedNextLine(createdNext, continuationGoal, mission) {
1165
+ const createdTask = createdNext?.ok ? createdNext.task : null;
1166
+ if (createdTask) {
1167
+ return `Created next task: ${missionRunTaskLabel(createdTask)}.`;
1168
+ }
1169
+ const activeTask = createdNext?.reason === 'active_task' ? createdNext.move : null;
1170
+ if (activeTask) {
1171
+ return `Continue active task: ${missionRunTaskLabel(activeTask)}.`;
1172
+ }
1173
+ return continuationGoal?.mission
1174
+ ? `Next mission: ${continuationGoal.mission.objective}.`
1175
+ : missionHumanNextAction(mission, process.cwd(), { allowSelfImprovementSeed: true });
1176
+ }
1177
+
1178
+ function missionRunTimelineCommand(mission) {
1179
+ return `atris mission timeline ${mission.id} --limit 5`;
1180
+ }
1181
+
1182
+ function missionRunExportCommand(mission) {
1183
+ return `atris mission timeline ${mission.id} --all --write`;
1184
+ }
1185
+
1186
+ function missionRunPrunePreviewCommand(mission) {
1187
+ return `atris mission timeline ${mission.id} --prune-preview`;
1188
+ }
1189
+
1190
+ function missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks = [], createdNext = null) {
1191
+ const stepChanged = missionLastStepSummary(ticks);
1192
+ const createdNextChanged = missionRunCreatedNextChangedText(createdNext);
1193
+ return createdNextChanged || stepChanged || missionFallbackChangedText(mission, mission.status, null, { ranTicks, effectiveMaxTicks });
1194
+ }
1195
+
1196
+ function missionBudgetLine(mission) {
1197
+ const contract = mission?.budget_contract;
1198
+ if (!contract) return null;
1199
+ return `${contract.plain_language} Limit: ${contract.budget_label}.`;
1200
+ }
1201
+
1202
+ function missionSpendsFullBudget(mission) {
1203
+ return mission?.budget_contract?.policy === 'spend_full_budget';
1204
+ }
1205
+
1206
+ function missionFullBudgetRemainingSeconds(mission, nowMs = Date.now()) {
1207
+ if (!missionSpendsFullBudget(mission)) return 0;
1208
+ const budgetSeconds = Number(mission?.budget_contract?.requested_seconds || mission?.max_wall_seconds || 0);
1209
+ if (!Number.isFinite(budgetSeconds) || budgetSeconds <= 0) return 0;
1210
+ const startedMs = Date.parse(mission.started_at || mission.created_at || mission.updated_at || '');
1211
+ if (!Number.isFinite(startedMs)) return 0;
1212
+ return Math.max(0, Math.ceil((startedMs + budgetSeconds * 1000 - nowMs) / 1000));
1213
+ }
1214
+
1215
+ function missionGoalChainIntent(text) {
1216
+ const lower = String(text || '').toLowerCase().replace(/\s+/g, ' ');
1217
+ return /\b(3\s*(?:or|-)?\s*4|three\s+or\s+four|multiple|child|subgoals?|goal\s+after\s+goal|keeps?\s+goaling|mission\s+feeling\s+good|feels?\s+good|validated\s+and\s+i\s+can\s+understand)\b/.test(lower)
1218
+ || /\bset\s+a\s+goal\b.{0,160}\b(accomplish|complete|finish|prove)\w*\b.{0,200}\bnext\s+(?:useful\s+)?goal\b/.test(lower);
1219
+ }
1220
+
1221
+ function missionGoalChainTargetCount(text) {
1222
+ const lower = String(text || '').toLowerCase();
1223
+ if (/\b3\s*(?:or|-)?\s*4\b|\bthree\s+or\s+four\b/.test(lower)) return 4;
1224
+ const explicit = lower.match(/\b([3-4])\s+(?:goals?|child\s+goals?|subgoals?)\b/);
1225
+ if (explicit) return Number(explicit[1]);
1226
+ return 4;
1227
+ }
1228
+
1229
+ function buildMissionGoalChain(objective, options = {}) {
1230
+ const targetCount = Math.max(3, Math.min(4, Number(options.targetCount || missionGoalChainTargetCount(objective)) || 4));
1231
+ const baseGoals = [
1232
+ {
1233
+ title: 'Find the novel mission',
1234
+ done_when: 'A concrete mission is named with why it matters now.',
1235
+ validation: 'Plain-English reason plus the source signal that made it worth doing.',
1236
+ },
1237
+ {
1238
+ title: 'Split it into child goals',
1239
+ done_when: 'The mission has a 3-4 step path with a clear first move.',
1240
+ validation: 'Each child goal says what proof would make it done.',
1241
+ },
1242
+ {
1243
+ title: 'Run the smallest proof',
1244
+ done_when: 'One real artifact, code change, status output, or receipt exists.',
1245
+ validation: 'A verifier, receipt, or inspectable output proves the work happened.',
1246
+ },
1247
+ {
1248
+ title: 'Explain the pause or next goal',
1249
+ done_when: 'The result says what changed, how it was checked, and whether to accept, revise, or continue.',
1250
+ validation: 'The operator can understand the mission state without reading logs.',
1251
+ },
1252
+ ].slice(0, targetCount).map((goal, index) => ({
1253
+ order: index + 1,
1254
+ status: 'pending',
1255
+ ...goal,
1256
+ }));
1257
+
1258
+ return {
1259
+ schema: 'atris.mission_goal_chain.v1',
1260
+ mode: 'validated_child_goals',
1261
+ status: 'planned',
1262
+ target_count: targetCount,
1263
+ done_count: 0,
1264
+ current_goal_order: 1,
1265
+ plain_language: 'One mission can be reached by several small goals, each with proof.',
1266
+ pause_rule: 'Pause when the chain has proof strong enough to understand, accept, revise, or choose the next mission.',
1267
+ validation_rule: 'Every child goal must leave a receipt, verifier result, artifact, or explicit stop reason.',
1268
+ goals: baseGoals,
1269
+ };
1270
+ }
1271
+
1272
+ function missionGoalChainCounts(goalChain) {
1273
+ const goals = Array.isArray(goalChain?.goals) ? goalChain.goals : [];
1274
+ const done = goals.filter((goal) => goal?.status === 'done').length;
1275
+ return { done, total: goals.length };
1276
+ }
1277
+
1278
+ function missionGoalChainPendingGoal(goalChain) {
1279
+ const goals = Array.isArray(goalChain?.goals) ? goalChain.goals : [];
1280
+ return goals.find((goal) => goal?.status !== 'done' && goal?.status !== 'blocked') || null;
1281
+ }
1282
+
1283
+ function missionGoalChainNextAction(goalChain) {
1284
+ const goal = missionGoalChainPendingGoal(goalChain);
1285
+ if (!goal) return 'continue child-goal chain';
1286
+ return `continue child goal ${goal.order}: ${goal.title}`;
1287
+ }
1288
+
1289
+ function advanceMissionGoalChain(goalChain, summary, verifierResult = null) {
1290
+ if (!goalChain || !Array.isArray(goalChain.goals) || !goalChain.goals.length) return goalChain || null;
1291
+ const cleanSummary = String(summary || '').replace(/\s+/g, ' ').trim();
1292
+ if (!cleanSummary && !verifierResult) return goalChain;
1293
+
1294
+ const goals = goalChain.goals.map((goal) => ({ ...goal }));
1295
+ const nextIndex = goals.findIndex((goal) => goal.status !== 'done');
1296
+ if (nextIndex === -1) return goalChain;
1297
+
1298
+ const validationResult = verifierResult
1299
+ ? (verifierResult.passed ? 'Verifier passed.' : 'Verifier failed; this goal needs repair.')
1300
+ : 'Summary receipt recorded.';
1301
+ goals[nextIndex] = {
1302
+ ...goals[nextIndex],
1303
+ status: verifierResult && !verifierResult.passed ? 'blocked' : 'done',
1304
+ completed_at: stampIso(),
1305
+ result: cleanSummary.slice(0, 240) || validationResult,
1306
+ validation_result: validationResult,
1307
+ };
1308
+
1309
+ const counts = missionGoalChainCounts({ goals });
1310
+ const blocked = goals.some((goal) => goal.status === 'blocked');
1311
+ const nextPending = goals.find((goal) => goal.status !== 'done' && goal.status !== 'blocked');
1312
+ return {
1313
+ ...goalChain,
1314
+ goals,
1315
+ done_count: counts.done,
1316
+ current_goal_order: nextPending ? nextPending.order : null,
1317
+ status: blocked ? 'blocked' : counts.done >= counts.total ? 'validated' : 'running',
1318
+ pause_ready: !blocked && counts.done >= counts.total,
1319
+ };
1320
+ }
1321
+
1322
+ function missionGoalChainLines(mission) {
1323
+ const chain = mission?.goal_chain;
1324
+ if (!chain || !Array.isArray(chain.goals) || !chain.goals.length) return [];
1325
+ const counts = missionGoalChainCounts(chain);
1326
+ const lines = [
1327
+ ` goal chain: ${counts.done}/${counts.total} done; ${chain.pause_rule}`,
1328
+ ];
1329
+ for (const goal of chain.goals) {
1330
+ const mark = goal.status === 'done' ? '[x]' : goal.status === 'blocked' ? '[!]' : '[ ]';
1331
+ lines.push(` ${mark} ${goal.order}. ${goal.title} -> ${goal.validation}`);
1332
+ }
1333
+ return lines;
1334
+ }
1335
+
1336
+ function missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalReceipt, pauseReason = null, continuationGoal = null, ticks = [], createdNext = null) {
1337
+ const changed = missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks, createdNext);
1338
+ const reason = missionHumanReasonText(mission, changed);
1339
+ const verifier = mission.verifier_result;
1340
+ const checked = verifier
1341
+ ? missionVerifierCheckedText(verifier, mission)
1342
+ : pauseReason
1343
+ ? `Run paused: ${pauseReason}.`
1344
+ : 'Run recorded; no verifier was run.';
1345
+ const tested = verifier
1346
+ ? missionVerifierHighLevelTestText(verifier, mission)
1347
+ : mission.verifier
1348
+ ? `Verifier was configured but not completed: ${mission.verifier}.`
1349
+ : 'No verifier command was recorded for this mission.';
1350
+ const nextLine = missionRunCreatedNextLine(createdNext, continuationGoal, mission);
1351
+ const lines = [
1352
+ 'Landing:',
1353
+ ` Changed: ${changed}`,
1354
+ ` Why it matters: ${reason}`,
1355
+ ...(missionBudgetLine(mission) ? [` Budget: ${missionBudgetLine(mission)}`] : []),
1356
+ ` How I checked: ${checked}`,
1357
+ ` What I tested: ${tested}`,
1358
+ ` Proof: Summary receipt saved at ${finalReceipt}.`,
1359
+ ` Timeline: ${missionRunTimelineCommand(mission)}`,
1360
+ ` Export: ${missionRunExportCommand(mission)}`,
1361
+ ` Prune preview: ${missionRunPrunePreviewCommand(mission)}`,
1362
+ ` Next: ${nextLine}`,
1363
+ ];
659
1364
  return lines;
660
1365
  }
661
1366
 
@@ -761,6 +1466,7 @@ function missionTaskSpine(mission) {
761
1466
  const taskRef = mission.xp_task?.ref
762
1467
  || mission.task_ref
763
1468
  || (taskId ? String(taskId) : null);
1469
+ const taskScopeRef = mission.task_scope_ref || mission.id;
764
1470
  const owner = ownerResolution.owner;
765
1471
  return {
766
1472
  schema: 'atris.mission_task_spine.v1',
@@ -776,7 +1482,7 @@ function missionTaskSpine(mission) {
776
1482
  task_ref: taskRef,
777
1483
  has_task: Boolean(taskId || taskRef),
778
1484
  current_step_command: taskId || taskRef
779
- ? `atris task current-step --goal-id ${mission.id} --as ${owner} --proof "<proof>" --json`
1485
+ ? `atris task current-step --goal-id ${taskScopeRef} --as ${owner} --proof "<proof>" --json`
780
1486
  : null,
781
1487
  ensure_task_command: taskId || taskRef
782
1488
  ? null
@@ -802,6 +1508,7 @@ function missionStatusView(mission) {
802
1508
  current_task_id: taskSpine.current_task_id,
803
1509
  task_ref: taskSpine.task_ref,
804
1510
  task_spine: taskSpine,
1511
+ last_landing: missionLastLanding(mission),
805
1512
  };
806
1513
  }
807
1514
 
@@ -814,13 +1521,22 @@ function missionFromArgs(args) {
814
1521
  '--lane',
815
1522
  '--verify',
816
1523
  '--stop',
1524
+ '--max-wall',
1525
+ '--minutes',
1526
+ '--hours',
1527
+ '--base',
817
1528
  '--task',
818
1529
  '--ask',
819
1530
  '--model',
820
- ], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree']).join(' ').trim();
1531
+ '--native-goal-status',
1532
+ '--native-goal-objective',
1533
+ '--visible-goal-status',
1534
+ '--visible-goal-objective',
1535
+ ], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--allow-native-goal-supersede', '--supersede-paused-native-goal']).join(' ').trim();
821
1536
  if (!objective) {
822
1537
  exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
823
1538
  }
1539
+ const budgetContract = inferRunObjectiveBudgetContract(objective, args);
824
1540
  const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
825
1541
  const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
826
1542
  const runner = readFlag(args, '--runner', 'manual');
@@ -837,7 +1553,7 @@ function missionFromArgs(args) {
837
1553
  const owner = ownerResolution.owner;
838
1554
  const verifier = readFlag(args, '--verify', '');
839
1555
  assertMissionVerifier(verifier, wantsJson(args));
840
- const stopCondition = readFlag(args, '--stop', verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof');
1556
+ const stopCondition = readFlag(args, '--stop', budgetStopCondition(budgetContract) || (verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof'));
841
1557
  const taskIds = readRepeatedFlag(args, '--task');
842
1558
  const humanAsks = readRepeatedFlag(args, '--ask');
843
1559
  const alwaysOn = hasFlag(args, '--always-on');
@@ -872,12 +1588,16 @@ function missionFromArgs(args) {
872
1588
  created_at: stampIso(),
873
1589
  updated_at: stampIso(),
874
1590
  };
1591
+ if (budgetContract) {
1592
+ mission.budget_contract = budgetContract;
1593
+ if (budgetContract.requested_seconds) mission.max_wall_seconds = budgetContract.requested_seconds;
1594
+ }
875
1595
  if (alwaysOn) mission.next_action = nextCandidateTickAction(mission);
876
1596
  return mission;
877
1597
  }
878
1598
 
879
1599
  function missingVerifierWarning(mission) {
880
- if (String(mission.verifier || '').trim()) return null;
1600
+ if (effectiveMissionVerifier(mission)) return null;
881
1601
  return {
882
1602
  code: 'missing_verifier',
883
1603
  message: 'Mission has no verifier; it cannot complete automatically and future runs will report unverified worktree side effects.',
@@ -899,6 +1619,9 @@ function missionRunSmokeVerifier() {
899
1619
  "if(p.action!=='mission_run_started')process.exit(2)",
900
1620
  "if(p.mission?.runner!=='codex_goal')process.exit(3)",
901
1621
  "if(p.codex_goal_state?.goal?.visible_goal?.schema!=='atris.visible_chat_goal_bridge.v1')process.exit(4)",
1622
+ "if(p.requires_native_goal_start!==true)process.exit(5)",
1623
+ "if(p.native_goal_action?.tool!=='create_goal')process.exit(6)",
1624
+ "if(!/create_goal/.test(p.next_command||''))process.exit(7)",
902
1625
  ].join(';');
903
1626
  return `node -e ${JSON.stringify(script)}`;
904
1627
  }
@@ -914,57 +1637,844 @@ function inferRunObjectiveVerifier(objective, root = process.cwd()) {
914
1637
  return missionRunSmokeVerifier();
915
1638
  }
916
1639
 
917
- function markMissionRunContinuation(mission) {
1640
+ function durationSecondsFromText(text) {
1641
+ const match = String(text || '').match(/\b(\d+(?:\.\d+)?)[-\s]*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)\b/i);
1642
+ if (!match) return null;
1643
+ const value = Number(match[1]);
1644
+ if (!Number.isFinite(value) || value <= 0) return null;
1645
+ const unit = match[2].toLowerCase();
1646
+ if (/^d/.test(unit)) return Math.round(value * 86400);
1647
+ if (/^h/.test(unit)) return Math.round(value * 3600);
1648
+ if (/^m(?!s)/.test(unit)) return Math.round(value * 60);
1649
+ return Math.round(value);
1650
+ }
1651
+
1652
+ function durationLabel(seconds, fallback = 'the requested time') {
1653
+ const value = Number(seconds);
1654
+ if (!Number.isFinite(value) || value <= 0) return fallback;
1655
+ if (value % 86400 === 0) {
1656
+ const days = value / 86400;
1657
+ return `${days} day${days === 1 ? '' : 's'}`;
1658
+ }
1659
+ if (value % 3600 === 0) {
1660
+ const hours = value / 3600;
1661
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
1662
+ }
1663
+ if (value % 60 === 0) {
1664
+ const minutes = value / 60;
1665
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
1666
+ }
1667
+ return `${value} second${value === 1 ? '' : 's'}`;
1668
+ }
1669
+
1670
+ function wantsFullBudget(text, args = []) {
1671
+ if (hasFlag(args, '--spend-full-budget') || hasFlag(args, '--use-whole-budget')) return true;
1672
+ if (hasFlag(args, '--hours') || hasFlag(args, '--minutes')) return true;
1673
+ const compact = String(text || '').toLowerCase().replace(/\s+/g, ' ');
1674
+ const duration = '\\d+(?:\\.\\d+)?[-\\s]*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)';
1675
+ return /\b(use|spend)\s+(the\s+)?(whole|full|entire)\s+(time|budget|window)\b/.test(compact)
1676
+ || /\b(use|spend)\s+(the\s+)?(whole|full|entire)\s+\d/.test(compact)
1677
+ || new RegExp(`\\b(?:spend|use)\\s+${duration}\\b`).test(compact)
1678
+ || new RegExp(`(?:^|\\b)(?:run|work|think|research|investigate|brainstorm|plan|map|audit|explore|study|analyze|build)\\s+(?:[^.!?;]{0,80}\\s+)?for\\s+${duration}\\b`).test(compact)
1679
+ || new RegExp(`^\\s*for\\s+${duration}\\b`).test(compact)
1680
+ || /\bfor\s+the\s+(whole|full|entire)\s+/.test(compact)
1681
+ || /\bkeep\s+going\s+until\s+(time|the\s+time|budget|the\s+budget)\s+(is\s+)?(up|done|spent|used)\b/.test(compact)
1682
+ || /\b(run|work|stop|continue)\s+(until|till)\s+(the\s+)?(time|budget)\s+(is\s+)?(up|done|spent|used)\b/.test(compact);
1683
+ }
1684
+
1685
+ function sleepLengthBudgetIntent(requestedSeconds, text = '') {
1686
+ const seconds = Number(requestedSeconds);
1687
+ return /\b(overnight|while\s+i\s+(?:sleep|am\s+sleeping|['’]?m\s+sleeping)|sleep(?:ing)?\s+run)\b/i.test(text)
1688
+ || (Number.isFinite(seconds) && seconds >= SLEEP_LENGTH_BUDGET_SECONDS);
1689
+ }
1690
+
1691
+ function inferRunObjectiveBudgetContract(objective, args = []) {
1692
+ const text = `${objective || ''} ${Array.isArray(args) ? args.join(' ') : ''}`;
1693
+ const explicitHours = Number(readFlag(args, '--hours', ''));
1694
+ const explicitMinutes = Number(readFlag(args, '--minutes', ''));
1695
+ const explicitMaxWall = Number(readFlag(args, '--max-wall', ''));
1696
+ const explicitSeconds = Number.isFinite(explicitHours) && explicitHours > 0
1697
+ ? Math.round(explicitHours * 3600)
1698
+ : (Number.isFinite(explicitMinutes) && explicitMinutes > 0
1699
+ ? Math.round(explicitMinutes * 60)
1700
+ : (Number.isFinite(explicitMaxWall) && explicitMaxWall > 0 ? Math.round(explicitMaxWall) : null));
1701
+ const requestedSeconds = explicitSeconds || durationSecondsFromText(text);
1702
+ const overnight = /\bovernight\b/i.test(text);
1703
+ if (!requestedSeconds && !overnight) return null;
1704
+ const spendBudget = !hasFlag(args, '--stop-when-done')
1705
+ && (wantsFullBudget(text, args) || sleepLengthBudgetIntent(requestedSeconds, text));
1706
+ const policy = spendBudget
1707
+ ? 'spend_full_budget'
1708
+ : 'stop_when_done';
1709
+ const budgetLabel = requestedSeconds
1710
+ ? durationLabel(requestedSeconds)
1711
+ : 'the overnight window';
1712
+ const plainLanguage = policy === 'spend_full_budget'
1713
+ ? 'Use the whole time.'
1714
+ : 'Finish early if solved.';
1715
+ const stopRule = policy === 'spend_full_budget'
1716
+ ? `Use the whole ${budgetLabel}; keep picking the next useful move until time is up, unless blocked or unsafe.`
1717
+ : `Use up to ${budgetLabel}; stop early when the mission is done, proven, or blocked.`;
918
1718
  return {
919
- ...mission,
920
- started_from: 'mission_run_objective',
921
- continue_on_complete: true,
922
- continuation_policy: 'decide_and_start_next_useful_mission',
1719
+ schema: 'atris.mission_budget_contract.v1',
1720
+ requested_seconds: requestedSeconds,
1721
+ budget_label: budgetLabel,
1722
+ policy,
1723
+ plain_language: plainLanguage,
1724
+ stop_rule: stopRule,
923
1725
  };
924
1726
  }
925
1727
 
926
- function continuationObjective(parent) {
927
- return `Decide and start the next useful mission after: ${parent.objective}`;
1728
+ function budgetStopCondition(contract) {
1729
+ if (!contract) return '';
1730
+ if (contract.policy === 'spend_full_budget') {
1731
+ return `run for ${contract.budget_label}; use the whole time unless blocked or unsafe`;
1732
+ }
1733
+ return `run for ${contract.budget_label}, or stop early when proof is strong enough`;
928
1734
  }
929
1735
 
930
- function findActiveContinuationMission(parent, root = process.cwd()) {
931
- return listMissions(root).find((mission) => (
932
- mission.parent_mission_id === parent.id
933
- && mission.started_from === 'mission_run_continuation'
934
- && !TERMINAL_STATUSES.has(mission.status)
935
- )) || null;
1736
+ function inferRunObjectiveLoopOptions(objective, args = []) {
1737
+ const text = `${objective || ''} ${Array.isArray(args) ? args.join(' ') : ''}`;
1738
+ const hoursMatch = text.match(/\b(\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours)\b/i);
1739
+ const requestedHours = hoursMatch ? Number(hoursMatch[1]) : null;
1740
+ const wantsLongRun = /\b(overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve)\b/i.test(text)
1741
+ || (Number.isFinite(requestedHours) && requestedHours > 0);
1742
+ if (!wantsLongRun) return { wantsLongRun: false, requestedHours: null, cadence: '' };
1743
+ return {
1744
+ wantsLongRun: true,
1745
+ requestedHours: Number.isFinite(requestedHours) && requestedHours > 0 ? requestedHours : null,
1746
+ cadence: readFlag(args, '--cadence', '13m'),
1747
+ };
936
1748
  }
937
1749
 
938
- function findActiveMissionRunContinuation(root = process.cwd(), excludeId = '') {
939
- const excluded = String(excludeId || '');
940
- const candidates = listMissions(root)
941
- .filter((mission) => (
942
- mission.id !== excluded
943
- && mission.started_from === 'mission_run_continuation'
944
- && mission.continuation_policy === 'choose_next_mission'
945
- && !TERMINAL_STATUSES.has(mission.status)
946
- ));
947
- candidates.sort((a, b) => missionSortTime(b) - missionSortTime(a));
948
- return candidates[0] || null;
1750
+ function missionLongRunIntent(mission) {
1751
+ const text = `${mission?.objective || ''} ${mission?.stop_condition || ''}`;
1752
+ return Boolean(mission?.overnight_loop)
1753
+ || /\b(overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve)\b/i.test(text);
949
1754
  }
950
1755
 
951
- function completeActiveContinuationForStartedMission(nextMission, root = process.cwd()) {
952
- const continuation = findActiveMissionRunContinuation(root, nextMission?.id);
953
- if (!continuation || !nextMission) return null;
954
- if (continuation.objective === nextMission.objective) return null;
1756
+ function missionChoosesNextMission(mission) {
1757
+ return mission?.started_from === 'mission_run_continuation'
1758
+ && mission?.continuation_policy === 'choose_next_mission';
1759
+ }
955
1760
 
956
- const proof = `Started next mission ${nextMission.id}: ${nextMission.objective}`;
957
- const completionGate = { ok: true, source: 'mission_run_continuation', forced: false };
958
- const baseNext = {
959
- ...continuation,
960
- status: 'complete',
961
- completed_at: stampIso(),
962
- proof,
963
- completion_gate: completionGate,
964
- continued_by_mission_id: nextMission.id,
965
- continued_by_objective: nextMission.objective,
966
- next_action: 'mission complete',
967
- };
1761
+ function isConcreteContinuationTarget(title) {
1762
+ const text = String(title || '').trim();
1763
+ if (!text) return false;
1764
+ if (/^decide and start the next useful mission after:/i.test(text)) return false;
1765
+ if (/<next useful mission>/i.test(text)) return false;
1766
+ if (/^create the next proof-backed self-improvement task$/i.test(text)) return false;
1767
+ return true;
1768
+ }
1769
+
1770
+ function continuationTargetKey(title) {
1771
+ return String(title || '')
1772
+ .replace(/^mission xp:\s*/i, '')
1773
+ .replace(/\s+/g, ' ')
1774
+ .trim()
1775
+ .toLowerCase();
1776
+ }
1777
+
1778
+ function handledContinuationTargetKeys(root, moves) {
1779
+ const keys = new Set();
1780
+ const add = (title) => {
1781
+ const key = continuationTargetKey(title);
1782
+ if (key) keys.add(key);
1783
+ };
1784
+ try {
1785
+ for (const title of moves.readHandledTaskTitles(root)) add(title);
1786
+ } catch {
1787
+ // Best-effort guard; mission state below still catches completed missions.
1788
+ }
1789
+ try {
1790
+ for (const row of listMissions(root)) {
1791
+ add(row.objective);
1792
+ }
1793
+ } catch {
1794
+ // If mission state is unreadable, fall back to the remaining explicit filters.
1795
+ }
1796
+ return keys;
1797
+ }
1798
+
1799
+ function readTaskProjectionForMission(root = process.cwd()) {
1800
+ const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
1801
+ try {
1802
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
1803
+ return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
1804
+ } catch {
1805
+ return [];
1806
+ }
1807
+ }
1808
+
1809
+ function taskTags(task) {
1810
+ return [
1811
+ task?.tag,
1812
+ ...(Array.isArray(task?.tags) ? task.tags : []),
1813
+ ...(Array.isArray(task?.metadata?.tags) ? task.metadata.tags : []),
1814
+ ]
1815
+ .filter(Boolean)
1816
+ .map((tag) => String(tag).trim().toLowerCase())
1817
+ .filter(Boolean);
1818
+ }
1819
+
1820
+ function certifiedReviewNextTaskCandidates(root = process.cwd()) {
1821
+ return readTaskProjectionForMission(root)
1822
+ .filter((task) => String(task?.status || '').toLowerCase() === 'review')
1823
+ .filter((task) => task?.review?.agent_certified === true || task?.metadata?.agent_certified === true)
1824
+ .map((task) => ({
1825
+ title: task?.review?.next_task || task?.metadata?.latest_agent_next_task || '',
1826
+ why: `certified review ${task.display_id || task.id || ''} suggested this next task`.trim(),
1827
+ source: 'certified_review_next_task',
1828
+ ref: task.display_id || task.id || null,
1829
+ weight: 95,
1830
+ }))
1831
+ .filter((move) => String(move.title || '').trim());
1832
+ }
1833
+
1834
+ function endgameBacklogCandidates(root = process.cwd()) {
1835
+ return readTaskProjectionForMission(root)
1836
+ .filter((task) => String(task?.status || '').toLowerCase() === 'open')
1837
+ .filter((task) => taskTags(task).includes('endgame'))
1838
+ .map((task) => ({
1839
+ title: String(task.title || '').trim(),
1840
+ why: `open endgame backlog task ${task.display_id || task.id || ''}`.trim(),
1841
+ source: 'endgame_backlog',
1842
+ ref: task.display_id || task.id || null,
1843
+ weight: 85,
1844
+ }))
1845
+ .filter((move) => move.title);
1846
+ }
1847
+
1848
+ function cleanWikiNextIngestTitle(value) {
1849
+ return String(value || '')
1850
+ .replace(/`([^`]+)`/g, '$1')
1851
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
1852
+ .replace(/^[-*]\s*/, '')
1853
+ .replace(/^next[- ]?ingests?\s*:\s*/i, '')
1854
+ .replace(/^next source\s*:\s*/i, '')
1855
+ .replace(/\s+/g, ' ')
1856
+ .trim()
1857
+ .replace(/[.!?:;,]+$/g, '');
1858
+ }
1859
+
1860
+ function wikiStatusNextIngestCandidates(root = process.cwd()) {
1861
+ const file = path.join(root, 'atris', 'wiki', 'STATUS.md');
1862
+ let text = '';
1863
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
1864
+ const out = [];
1865
+ let inSection = false;
1866
+ for (const rawLine of text.split(/\r?\n/)) {
1867
+ const line = rawLine.trim();
1868
+ if (/^#+\s+next[- ]?ingests?\b/i.test(line)) {
1869
+ inSection = true;
1870
+ continue;
1871
+ }
1872
+ if (inSection && /^#+\s+/.test(line)) inSection = false;
1873
+ const direct = line.match(/^[-*]?\s*next[- ]?ingests?\s*:\s*(.+)$/i)
1874
+ || line.match(/^[-*]?\s*next source\s*:\s*(.+)$/i);
1875
+ if (direct) {
1876
+ const title = cleanWikiNextIngestTitle(direct[1]);
1877
+ if (title) out.push(title);
1878
+ continue;
1879
+ }
1880
+ if (inSection && /^[-*]\s+/.test(line)) {
1881
+ const title = cleanWikiNextIngestTitle(line);
1882
+ if (title) out.push(title);
1883
+ }
1884
+ }
1885
+ return out.map((title) => ({
1886
+ title: /^ingest\b/i.test(title) ? title : `Ingest ${title}`,
1887
+ why: 'wiki STATUS listed this as a next ingest',
1888
+ source: 'wiki_status_next_ingest',
1889
+ ref: 'atris/wiki/STATUS.md',
1890
+ weight: 70,
1891
+ }));
1892
+ }
1893
+
1894
+ function missionExtraNextCandidates(root = process.cwd()) {
1895
+ return [
1896
+ ...certifiedReviewNextTaskCandidates(root),
1897
+ ...endgameBacklogCandidates(root),
1898
+ ...wikiStatusNextIngestCandidates(root),
1899
+ ];
1900
+ }
1901
+
1902
+ function uniqueMissionCandidates(candidates) {
1903
+ const seen = new Set();
1904
+ const out = [];
1905
+ for (const candidate of candidates) {
1906
+ const key = continuationTargetKey(candidate?.title);
1907
+ if (!key || seen.has(key)) continue;
1908
+ seen.add(key);
1909
+ out.push(candidate);
1910
+ }
1911
+ return out;
1912
+ }
1913
+
1914
+ function trustedNextMissionSource(source) {
1915
+ return new Set([
1916
+ 'certified_review_next_task',
1917
+ 'endgame_backlog',
1918
+ 'wiki_status_next_ingest',
1919
+ 'mission_report',
1920
+ ]).has(String(source || ''));
1921
+ }
1922
+
1923
+ function rejectNextMissionCandidate(move, context) {
1924
+ const title = String(move?.title || '').trim();
1925
+ if (!title) return 'empty title';
1926
+ if (context.moves.isGenericInboxPlaceholder(title)) return 'placeholder';
1927
+ if (!isConcreteContinuationTarget(title)) return 'not a concrete mission';
1928
+ const key = continuationTargetKey(title);
1929
+ if (context.handledTargets.has(key)) return 'already handled';
1930
+ if (context.currentObjective && title === context.currentObjective) return 'same as current objective';
1931
+ if (context.parentObjective && title === context.parentObjective) return 'same as parent objective';
1932
+ if (context.mission?.id && move?.ref === context.mission.id && title === context.currentObjective) return 'same as current mission';
1933
+ const preview = missionValuePreview(move, context.mission, context.root);
1934
+ const score = Number(preview?.score?.total || 0);
1935
+ move.value_preview = preview;
1936
+ if (score <= 0 && !trustedNextMissionSource(move.source)) return 'zero value score';
1937
+ return '';
1938
+ }
1939
+
1940
+ function nearMissPreview(nearMisses) {
1941
+ if (!nearMisses.length) return null;
1942
+ return {
1943
+ schema: 'atris.next_mission_stop_preview.v1',
1944
+ stop_reason: 'no concrete follow-up mission found in Atris state',
1945
+ near_misses: nearMisses.slice(0, 3),
1946
+ feynman: {
1947
+ what: 'Atris found possible next moves, but none passed the mission filter.',
1948
+ why_now: 'Stopping is safer than spending tokens on fake or low-value work.',
1949
+ risk: 'A human may want to promote one near-miss manually.',
1950
+ validation: 'Review the near-miss reasons, then add a concrete next task if one should run.',
1951
+ },
1952
+ };
1953
+ }
1954
+
1955
+ function chooseNextMissionAnalysis(mission, root = process.cwd()) {
1956
+ try {
1957
+ const moves = require('../lib/next-moves');
1958
+ const currentObjective = String(mission?.objective || '').trim();
1959
+ const parentObjective = String(mission?.parent_objective || '').trim();
1960
+ const handledTargets = handledContinuationTargetKeys(root, moves);
1961
+ const latestTarget = moves.latestSuggestedTarget(root);
1962
+ const reportCandidates = latestTarget
1963
+ ? [{
1964
+ title: latestTarget,
1965
+ why: 'latest proof timeline suggested this follow-up mission',
1966
+ source: 'mission_report',
1967
+ weight: 75,
1968
+ }]
1969
+ : [];
1970
+ const context = { mission, root, moves, currentObjective, parentObjective, handledTargets };
1971
+ const nearMisses = [];
1972
+ const candidates = uniqueMissionCandidates([
1973
+ ...missionExtraNextCandidates(root),
1974
+ ...reportCandidates,
1975
+ ...moves.nextMoves(root, 8),
1976
+ ])
1977
+ .filter((move) => {
1978
+ const reason = rejectNextMissionCandidate(move, context);
1979
+ if (reason) {
1980
+ nearMisses.push({
1981
+ title: String(move?.title || '').trim() || '(empty)',
1982
+ source: move?.source || null,
1983
+ ref: move?.ref || null,
1984
+ reason,
1985
+ });
1986
+ return false;
1987
+ }
1988
+ return true;
1989
+ })
1990
+ .sort((a, b) => {
1991
+ const aScore = Number(a.value_preview?.score?.total || 0);
1992
+ const bScore = Number(b.value_preview?.score?.total || 0);
1993
+ if (aScore !== bScore) return bScore - aScore;
1994
+ return Number(b.weight || 0) - Number(a.weight || 0);
1995
+ });
1996
+ return { target: candidates[0] || null, near_misses: nearMisses.slice(0, 3) };
1997
+ } catch {
1998
+ // Fall through to an explicit stop command; never emit the old placeholder.
1999
+ }
2000
+ return { target: null, near_misses: [] };
2001
+ }
2002
+
2003
+ function chooseNextMissionTarget(mission, root = process.cwd()) {
2004
+ return chooseNextMissionAnalysis(mission, root).target;
2005
+ }
2006
+
2007
+ function normalizeMissionOwner(value) {
2008
+ return String(value || '').trim().toLowerCase();
2009
+ }
2010
+
2011
+ function missionMemberTasteProfile(owner) {
2012
+ const key = normalizeMissionOwner(owner);
2013
+ if (/\b(security|sync-inspector|proof-inspector|validator)\b/.test(key)) {
2014
+ return {
2015
+ id: 'security_guard',
2016
+ name: 'Security / trust',
2017
+ bias: 'Prefer work that prevents unsafe behavior, privacy mistakes, or quiet trust breaks.',
2018
+ };
2019
+ }
2020
+ if (/\b(researcher|architect|improver|auto-improver|problem-solver|objective-generator)\b/.test(key)) {
2021
+ return {
2022
+ id: 'technical_homerun',
2023
+ name: 'Technical homerun',
2024
+ bias: 'Prefer technical leaps, but require a usability or proof gate before spending serious tokens.',
2025
+ };
2026
+ }
2027
+ return {
2028
+ id: 'usability_operator',
2029
+ name: 'Usability / demo',
2030
+ bias: 'Prefer work that makes the product easier, faster, clearer, or more demo-ready.',
2031
+ };
2032
+ }
2033
+
2034
+ function missionValueSignals(title) {
2035
+ const text = String(title || '').toLowerCase();
2036
+ const signals = [];
2037
+ const add = (id, label, pattern) => {
2038
+ if (pattern.test(text)) signals.push({ id, label });
2039
+ };
2040
+ add('usability', 'simplifies use', /\b(simple|simplify|preview|clear|plain|feynman|demo|onboarding|ux|workflow|usable|understand)\b/);
2041
+ add('speed', 'speeds the process', /\b(speed|fast|faster|latency|token|waste|friction|shortcut|automation|auto)\b/);
2042
+ add('users_revenue', 'can help users or revenue', /\b(user|users|customer|revenue|payment|checkout|pricing|conversion|retention|demo)\b/);
2043
+ add('trust', 'protects trust', /\b(security|safe|safety|trust|permission|approval|auth|privacy|gmail|email|connector|leak|isolation)\b/);
2044
+ add('technical', 'technical advancement', /\b(agent|ax|connector|isolation|research|benchmark|model|compiler|runtime|architecture|rl|experiment)\b/);
2045
+ add('freshness', 'keeps workflow current', /\b(up[- ]?to[- ]?date|fresh|sync|stale|current|latest)\b/);
2046
+ return signals;
2047
+ }
2048
+
2049
+ function missionTasteSnippet(value, max = 180) {
2050
+ const text = String(value || '')
2051
+ .replace(/<!--[\s\S]*?-->/g, ' ')
2052
+ .replace(/[`*_#>]+/g, '')
2053
+ .replace(/^[-\d.()[\]\s]+/g, '')
2054
+ .replace(/\s+/g, ' ')
2055
+ .trim();
2056
+ if (!text) return '';
2057
+ return text.length > max ? `${text.slice(0, max - 3).trim()}...` : text;
2058
+ }
2059
+
2060
+ function missionTasteSnippets(text, limit = 4) {
2061
+ const tastePattern = /\b(plain|jargon|feynman|understand|simple|clarify|clarity|proof|receipt|verify|verified|checked|tested|accept|approval|runway|revenue|cash|user|customer|demo|technical|homerun|security|trust|privacy|logs|working memory|recent|speed|token|waste|simplify)\b/i;
2062
+ const snippets = [];
2063
+ for (const line of String(text || '').split(/\r?\n/)) {
2064
+ const snippet = missionTasteSnippet(line);
2065
+ if (!snippet || !tastePattern.test(snippet)) continue;
2066
+ snippets.push(snippet);
2067
+ }
2068
+ return snippets.slice(-limit);
2069
+ }
2070
+
2071
+ function readTextFile(root, relativePath) {
2072
+ const file = path.join(root, ...relativePath.split('/'));
2073
+ try {
2074
+ return { path: relativePath, present: true, text: fs.readFileSync(file, 'utf8') };
2075
+ } catch {
2076
+ return { path: relativePath, present: false, text: '' };
2077
+ }
2078
+ }
2079
+
2080
+ function recentMarkdownFiles(dir, limit = 2) {
2081
+ const files = [];
2082
+ try {
2083
+ for (const entry of fs.readdirSync(dir)) {
2084
+ const full = path.join(dir, entry);
2085
+ const stat = fs.statSync(full);
2086
+ if (stat.isDirectory()) {
2087
+ for (const child of recentMarkdownFiles(full, limit * 4)) files.push(child);
2088
+ } else if (/\.md$/i.test(entry)) {
2089
+ files.push(full);
2090
+ }
2091
+ }
2092
+ } catch {
2093
+ return [];
2094
+ }
2095
+ return files
2096
+ .sort((a, b) => {
2097
+ const aBase = path.basename(a);
2098
+ const bBase = path.basename(b);
2099
+ if (aBase !== bBase) return aBase.localeCompare(bBase);
2100
+ return a.localeCompare(b);
2101
+ })
2102
+ .slice(-limit);
2103
+ }
2104
+
2105
+ function readRecentTasteLogs(root, owner, limit = 3) {
2106
+ const logFiles = [
2107
+ ...recentMarkdownFiles(path.join(root, 'atris', 'logs'), 2),
2108
+ ...recentMarkdownFiles(path.join(root, 'atris', 'team', owner || '', 'logs'), 2),
2109
+ ];
2110
+ const seen = new Set();
2111
+ return logFiles
2112
+ .filter((file) => {
2113
+ const key = path.resolve(file);
2114
+ if (seen.has(key)) return false;
2115
+ seen.add(key);
2116
+ return true;
2117
+ })
2118
+ .slice(-limit)
2119
+ .map((file) => {
2120
+ let text = '';
2121
+ try { text = fs.readFileSync(file, 'utf8'); } catch { text = ''; }
2122
+ return {
2123
+ path: path.relative(root, file),
2124
+ snippets: missionTasteSnippets(text, 3),
2125
+ };
2126
+ })
2127
+ .filter((entry) => entry.snippets.length);
2128
+ }
2129
+
2130
+ function readTasteReviewHistory(root, limit = 4) {
2131
+ const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
2132
+ let projection = null;
2133
+ try { projection = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { projection = null; }
2134
+ const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
2135
+ const accepted = [];
2136
+ const revised = [];
2137
+ const sorted = tasks
2138
+ .filter(Boolean)
2139
+ .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
2140
+ for (const task of sorted) {
2141
+ const title = missionTasteSnippet(task.title, 120);
2142
+ for (const event of Array.isArray(task.events) ? task.events : []) {
2143
+ const type = String(event?.event_type || '');
2144
+ const payload = event?.payload || {};
2145
+ if ((type === 'accepted' || (type === 'reviewed' && Number(payload.reward || 0) > 0)) && accepted.length < limit) {
2146
+ accepted.push(missionTasteSnippet(`${title}: ${payload.proof || payload.note || 'accepted'}`));
2147
+ }
2148
+ if (type === 'revision_requested' && revised.length < limit) {
2149
+ revised.push(missionTasteSnippet(`${title}: ${payload.note || 'revision requested'}`));
2150
+ }
2151
+ }
2152
+ }
2153
+ return { accepted, revised };
2154
+ }
2155
+
2156
+ function missionTasteMemorySignals(text) {
2157
+ const definitions = [
2158
+ ['plain_language', 'Keshav asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
2159
+ ['proof_gate', 'Keshav wants proof before accept', /\b(proof|receipt|verify|verified|checked|tested|accept|approval)\b/i],
2160
+ ['runway_revenue', 'runway pushes user or revenue work', /\b(runway|revenue|cash|user|users|customer|demo|adoption|retention)\b/i],
2161
+ ['technical_ambition', 'technical advancement still matters', /\b(technical|homerun|research|runtime|architecture|model|benchmark|agent)\b/i],
2162
+ ['trust_boundary', 'trust and approval boundaries matter', /\b(security|safe|safety|trust|privacy|approval|permission|gmail|email|connector)\b/i],
2163
+ ['working_memory', 'recent logs are working memory', /\b(logs?|working memory|recent|today|now)\b/i],
2164
+ ['speed_token', 'avoid wasted time and tokens', /\b(speed|fast|faster|token|waste|friction|simplify)\b/i],
2165
+ ];
2166
+ return definitions
2167
+ .filter(([, , pattern]) => pattern.test(text))
2168
+ .map(([id, label]) => ({ id, label }));
2169
+ }
2170
+
2171
+ function readMissionTasteMemory(root = process.cwd(), owner = '') {
2172
+ const safeOwner = normalizeMissionOwner(owner || process.env.ATRIS_AGENT_ID || 'mission-lead') || 'mission-lead';
2173
+ const thinking = readTextFile(root, 'atris/thinking.md');
2174
+ const memberMission = readTextFile(root, `atris/team/${safeOwner}/MISSION.md`);
2175
+ const recentLogs = readRecentTasteLogs(root, safeOwner);
2176
+ const taskHistory = readTasteReviewHistory(root);
2177
+ const thinkingSnippets = missionTasteSnippets(thinking.text, 5);
2178
+ const memberMissionSnippets = missionTasteSnippets(memberMission.text, 5);
2179
+ const allText = [
2180
+ ...thinkingSnippets,
2181
+ ...memberMissionSnippets,
2182
+ ...recentLogs.flatMap((entry) => entry.snippets),
2183
+ ...taskHistory.accepted,
2184
+ ...taskHistory.revised,
2185
+ ].join('\n');
2186
+ const signals = missionTasteMemorySignals(allText);
2187
+ return {
2188
+ schema: 'atris.mission_taste_memory.v1',
2189
+ owner: safeOwner,
2190
+ sources: {
2191
+ thinking_md: {
2192
+ path: thinking.path,
2193
+ present: thinking.present,
2194
+ snippets: thinkingSnippets,
2195
+ },
2196
+ member_mission: {
2197
+ path: memberMission.path,
2198
+ present: memberMission.present,
2199
+ snippets: memberMissionSnippets,
2200
+ },
2201
+ recent_logs: recentLogs,
2202
+ task_history: taskHistory,
2203
+ },
2204
+ signals,
2205
+ };
2206
+ }
2207
+
2208
+ function missionTasteMemoryBoost(signals, tasteMemory, profile) {
2209
+ const candidateIds = new Set((signals || []).map((signal) => signal.id));
2210
+ const memoryIds = new Set((tasteMemory?.signals || []).map((signal) => signal.id));
2211
+ const hasCandidate = (id) => candidateIds.has(id) ? 1 : 0;
2212
+ const hasMemory = (id) => memoryIds.has(id);
2213
+ let boost = 0;
2214
+ if (hasMemory('plain_language')) boost += hasCandidate('usability') * 2;
2215
+ if (hasMemory('proof_gate')) boost += (hasCandidate('trust') || hasCandidate('usability') || hasCandidate('technical')) ? 1 : 0;
2216
+ if (hasMemory('runway_revenue')) boost += hasCandidate('users_revenue') * 3;
2217
+ if (hasMemory('technical_ambition')) boost += profile.id === 'technical_homerun' ? hasCandidate('technical') * 2 : hasCandidate('technical');
2218
+ if (hasMemory('trust_boundary')) boost += hasCandidate('trust') * 2;
2219
+ if (hasMemory('working_memory')) boost += (hasCandidate('freshness') || hasCandidate('speed')) ? 1 : 0;
2220
+ if (hasMemory('speed_token')) boost += hasCandidate('speed') * 2;
2221
+ return boost;
2222
+ }
2223
+
2224
+ function missionValueScore(signals, profile, tasteMemory = null) {
2225
+ const ids = new Set((signals || []).map((signal) => signal.id));
2226
+ const has = (id) => ids.has(id) ? 1 : 0;
2227
+ const base = has('usability') + has('speed') + has('users_revenue') + has('trust') + has('freshness');
2228
+ let profileBoost = 0;
2229
+ if (profile.id === 'technical_homerun') profileBoost = (has('technical') * 2) + has('trust') + has('speed');
2230
+ else if (profile.id === 'security_guard') profileBoost = (has('trust') * 3) + has('freshness');
2231
+ else profileBoost = (has('usability') * 2) + has('speed') + has('users_revenue');
2232
+ const memoryBoost = missionTasteMemoryBoost(signals, tasteMemory, profile);
2233
+ return {
2234
+ total: base + profileBoost + memoryBoost,
2235
+ signals: Array.from(ids),
2236
+ memory_boost: memoryBoost,
2237
+ };
2238
+ }
2239
+
2240
+ function missionPlainTaskPreview(title) {
2241
+ const text = String(title || '').replace(/\s+/g, ' ').trim();
2242
+ if (!text) return 'Do one concrete useful thing.';
2243
+ if (/ax\b/i.test(text) && /gmail/i.test(text) && /turn isolation/i.test(text)) {
2244
+ return 'Make ax safer with Gmail: keep each chat request separate, and show a clear preview or receipt before Gmail actions.';
2245
+ }
2246
+ const cleaned = text
2247
+ .replace(/^mission xp:\s*/i, '')
2248
+ .replace(/^ship\s+/i, '')
2249
+ .replace(/\bturn isolation\b/ig, 'separate chat requests')
2250
+ .replace(/\breceipt previews?\b/ig, 'clear before/after receipts')
2251
+ .replace(/\bconnector\b/ig, 'connected-app path');
2252
+ return `${cleaned.charAt(0).toUpperCase()}${cleaned.slice(1)}`.replace(/[.!?:;,]+$/g, '') + '.';
2253
+ }
2254
+
2255
+ function missionTasteMemoryReason(tasteMemory, signals) {
2256
+ const candidateIds = new Set((signals || []).map((signal) => signal.id));
2257
+ const memoryIds = new Set((tasteMemory?.signals || []).map((signal) => signal.id));
2258
+ if (memoryIds.has('runway_revenue') && candidateIds.has('users_revenue')) return 'Keshav has been steering toward user/revenue proof because runway is tight.';
2259
+ if (memoryIds.has('plain_language') && candidateIds.has('usability')) return 'Keshav keeps asking for plain-English, understandable work.';
2260
+ if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return 'Keshav wants proof and receipts before serious work is accepted.';
2261
+ if (memoryIds.has('working_memory')) return 'Recent logs are being used as working memory for what matters now.';
2262
+ if (memoryIds.has('technical_ambition') && candidateIds.has('technical')) return 'The member memory still values technical advancement when it has proof.';
2263
+ return '';
2264
+ }
2265
+
2266
+ function missionPreviewWhyNow(move, profile, signals, tasteMemory = null) {
2267
+ const ids = new Set((signals || []).map((signal) => signal.id));
2268
+ let base = '';
2269
+ if (profile.id === 'technical_homerun') {
2270
+ base = ids.has('technical')
2271
+ ? 'This is a technical bet; run it only because it can make the product stronger without making the user think harder.'
2272
+ : 'This is not a technical homerun on its face, so it needs clear usability proof before it should win.';
2273
+ } else if (profile.id === 'security_guard') {
2274
+ base = ids.has('trust')
2275
+ ? 'This matters because trust failures are expensive and should be blocked before users feel them.'
2276
+ : 'This is not mainly security work, so it should only run if the current trust queue is quiet.';
2277
+ } else if (ids.has('users_revenue')) {
2278
+ base = 'This matters now because it can help demos, users, retention, or revenue.';
2279
+ } else if (ids.has('usability') || ids.has('speed')) {
2280
+ base = 'This matters now because it can make the product easier or faster to use.';
2281
+ } else {
2282
+ base = move?.why || 'This needs a clear reason before it should spend serious tokens.';
2283
+ }
2284
+ const memoryReason = missionTasteMemoryReason(tasteMemory, signals);
2285
+ return memoryReason ? `${base} Taste memory says: ${memoryReason}` : base;
2286
+ }
2287
+
2288
+ function missionPreviewRisk(signals) {
2289
+ const ids = new Set((signals || []).map((signal) => signal.id));
2290
+ if (ids.has('technical') && !ids.has('usability')) return 'Risk: it becomes clever infrastructure that does not make the product easier.';
2291
+ if (ids.has('trust')) return 'Risk: changing connected-tool behavior can hide or create approval/privacy mistakes.';
2292
+ return 'Risk: it adds complexity without a visible product win.';
2293
+ }
2294
+
2295
+ function missionPreviewValidation(signals) {
2296
+ const ids = new Set((signals || []).map((signal) => signal.id));
2297
+ if (ids.has('trust')) return 'Validate with a before/after receipt and a test proving unsafe state does not carry across turns.';
2298
+ if (ids.has('users_revenue')) return 'Validate with a customer-visible proof: demo path, signup/payment path, or retention signal.';
2299
+ if (ids.has('technical')) return 'Validate with a small benchmark, regression test, or artifact that proves the technical bet helped.';
2300
+ return 'Validate with a simple before/after check that a human can understand.';
2301
+ }
2302
+
2303
+ function missionValuePreview(move, mission, root = process.cwd()) {
2304
+ const profile = missionMemberTasteProfile(mission?.owner);
2305
+ const signals = missionValueSignals(move?.title);
2306
+ const tasteMemory = readMissionTasteMemory(root, mission?.owner);
2307
+ const score = missionValueScore(signals, profile, tasteMemory);
2308
+ return {
2309
+ schema: 'atris.mission_value_preview.v1',
2310
+ member: mission?.owner || null,
2311
+ profile,
2312
+ candidate: {
2313
+ title: move?.title || '',
2314
+ source: move?.source || null,
2315
+ ref: move?.ref || null,
2316
+ },
2317
+ feynman: {
2318
+ what: missionPlainTaskPreview(move?.title),
2319
+ why_now: missionPreviewWhyNow(move, profile, signals, tasteMemory),
2320
+ risk: missionPreviewRisk(signals),
2321
+ validation: missionPreviewValidation(signals),
2322
+ taste: missionTasteMemoryReason(tasteMemory, signals) || 'No live taste memory matched this candidate yet.',
2323
+ },
2324
+ value_signals: signals,
2325
+ taste_memory: tasteMemory,
2326
+ score,
2327
+ };
2328
+ }
2329
+
2330
+ function resumableActiveMissions(mission, root = process.cwd()) {
2331
+ const selfId = String(mission?.id || '');
2332
+ const parentId = String(mission?.parent_mission_id || '');
2333
+ try {
2334
+ // Only planning missions resume: ready missions wait for human review, and
2335
+ // running missions already have an actor.
2336
+ return listMissions(root).filter((row) => {
2337
+ if (!row || row.status !== 'planning') return false;
2338
+ if (row.id === selfId || row.id === parentId) return false;
2339
+ if (String(row.started_from || '') === 'mission_run_continuation') return false;
2340
+ if (!isConcreteContinuationTarget(row.objective)) return false;
2341
+ return true;
2342
+ });
2343
+ } catch {
2344
+ return [];
2345
+ }
2346
+ }
2347
+
2348
+ function chooseResumeMissionPlan(mission, nearMisses, root = process.cwd()) {
2349
+ const ranked = resumableActiveMissions(mission, root)
2350
+ .map((row) => ({
2351
+ row,
2352
+ preview: missionValuePreview({ title: row.objective, source: 'resume_active_mission', ref: row.id }, mission, root),
2353
+ }))
2354
+ .sort((a, b) => {
2355
+ const aScore = Number(a.preview?.score?.total || 0);
2356
+ const bScore = Number(b.preview?.score?.total || 0);
2357
+ if (aScore !== bScore) return bScore - aScore;
2358
+ return String(b.row.updated_at || '').localeCompare(String(a.row.updated_at || ''));
2359
+ });
2360
+ const best = ranked[0];
2361
+ if (!best) return null;
2362
+ return {
2363
+ target: {
2364
+ title: best.row.objective,
2365
+ source: 'resume_active_mission',
2366
+ ref: best.row.id,
2367
+ value_preview: best.preview,
2368
+ },
2369
+ command: `atris mission run ${best.row.id} --json`,
2370
+ preview: {
2371
+ schema: 'atris.next_mission_resume_preview.v1',
2372
+ resume_mission_id: best.row.id,
2373
+ resume_objective: best.row.objective,
2374
+ resume_status: best.row.status,
2375
+ resume_owner: best.row.owner || null,
2376
+ near_misses: nearMisses.slice(0, 3),
2377
+ feynman: {
2378
+ what: 'Every new candidate is already tracked, and one active mission is still in planning.',
2379
+ why_now: 'Resuming chosen work beats stopping when real work is already on the board.',
2380
+ risk: 'The resumed mission may deserve a fresher objective; stop it if it no longer matters.',
2381
+ validation: 'Run the resume command, then check the mission landing and receipt.',
2382
+ },
2383
+ },
2384
+ };
2385
+ }
2386
+
2387
+ function chooseNextMissionPlan(mission, root = process.cwd()) {
2388
+ const owner = mission?.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
2389
+ const analysis = chooseNextMissionAnalysis(mission, root);
2390
+ const target = analysis.target;
2391
+ if (target?.title) {
2392
+ return {
2393
+ target,
2394
+ command: `atris mission run ${shellQuote(target.title)} --owner ${owner}`,
2395
+ preview: target.value_preview || missionValuePreview(target, mission, root),
2396
+ };
2397
+ }
2398
+ const resume = chooseResumeMissionPlan(mission, analysis.near_misses || [], root);
2399
+ if (resume) return resume;
2400
+ const missionId = mission?.id || '<mission-id>';
2401
+ return {
2402
+ target: null,
2403
+ command: `atris mission stop ${missionId} --reason ${shellQuote('no concrete follow-up mission found in Atris state')} --json`,
2404
+ preview: nearMissPreview(analysis.near_misses || []),
2405
+ };
2406
+ }
2407
+
2408
+ function chooseNextMissionCommand(mission, root = process.cwd()) {
2409
+ return chooseNextMissionPlan(mission, root).command;
2410
+ }
2411
+
2412
+ function chooseNextMissionPreview(mission, root = process.cwd()) {
2413
+ return chooseNextMissionPlan(mission, root).preview;
2414
+ }
2415
+
2416
+ function effectiveMissionVerifier(mission) {
2417
+ const explicit = String(mission?.verifier || '').trim();
2418
+ if (explicit) return explicit;
2419
+ if (missionChoosesNextMission(mission)) return '';
2420
+ const runner = String(mission?.runner || '').trim().toLowerCase();
2421
+ if (missionLongRunIntent(mission) && (!runner || runner === 'codex_goal')) {
2422
+ return DEFAULT_LONG_RUN_VERIFIER;
2423
+ }
2424
+ return '';
2425
+ }
2426
+
2427
+ function markMissionRunContinuation(mission) {
2428
+ return {
2429
+ ...mission,
2430
+ started_from: 'mission_run_objective',
2431
+ continue_on_complete: true,
2432
+ continuation_policy: 'decide_and_start_next_useful_mission',
2433
+ };
2434
+ }
2435
+
2436
+ function continuationObjective(parent) {
2437
+ return `Decide and start the next useful mission after: ${parent.objective}`;
2438
+ }
2439
+
2440
+ function findActiveContinuationMission(parent, root = process.cwd()) {
2441
+ return listMissions(root).find((mission) => (
2442
+ mission.parent_mission_id === parent.id
2443
+ && mission.started_from === 'mission_run_continuation'
2444
+ && !TERMINAL_STATUSES.has(mission.status)
2445
+ )) || null;
2446
+ }
2447
+
2448
+ function findActiveMissionRunContinuation(root = process.cwd(), excludeId = '') {
2449
+ const excluded = String(excludeId || '');
2450
+ const candidates = listMissions(root)
2451
+ .filter((mission) => (
2452
+ mission.id !== excluded
2453
+ && mission.started_from === 'mission_run_continuation'
2454
+ && mission.continuation_policy === 'choose_next_mission'
2455
+ && !TERMINAL_STATUSES.has(mission.status)
2456
+ ));
2457
+ candidates.sort((a, b) => missionSortTime(b) - missionSortTime(a));
2458
+ return candidates[0] || null;
2459
+ }
2460
+
2461
+ function completeActiveContinuationForStartedMission(nextMission, root = process.cwd()) {
2462
+ const continuation = findActiveMissionRunContinuation(root, nextMission?.id);
2463
+ if (!continuation || !nextMission) return null;
2464
+ if (continuation.objective === nextMission.objective) return null;
2465
+
2466
+ const proof = `Started next mission ${nextMission.id}: ${nextMission.objective}`;
2467
+ const completionGate = { ok: true, source: 'mission_run_continuation', forced: false };
2468
+ const baseNext = {
2469
+ ...continuation,
2470
+ status: 'complete',
2471
+ completed_at: stampIso(),
2472
+ proof,
2473
+ completion_gate: completionGate,
2474
+ continued_by_mission_id: nextMission.id,
2475
+ continued_by_objective: nextMission.objective,
2476
+ next_action: 'mission complete',
2477
+ };
968
2478
  const completion = missionCompletionReceipt(baseNext, proof);
969
2479
  const { mission: saved } = saveMission({
970
2480
  ...baseNext,
@@ -990,26 +2500,47 @@ function completeActiveContinuationForStartedMission(nextMission, root = process
990
2500
  };
991
2501
  }
992
2502
 
2503
+ function missionCanSeedContinuation(parent) {
2504
+ if (!parent) return false;
2505
+ if (parent.status === 'complete') return true;
2506
+ return GOAL_LOOP_STATUSES.has(String(parent.status || '')) && missionTaskHumanAcceptWaiting(parent);
2507
+ }
2508
+
993
2509
  function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
994
- if (!parent || parent.status !== 'complete') return null;
2510
+ if (!missionCanSeedContinuation(parent)) return null;
995
2511
  if (parent.continue_on_complete !== true) return null;
996
- if (parent.continuation_seeded_mission_id) return {
997
- inserted: false,
998
- reason: 'already_seeded',
999
- mission_id: parent.continuation_seeded_mission_id,
1000
- };
2512
+ if (parent.continuation_seeded_mission_id) {
2513
+ const seeded = resolveMission(parent.continuation_seeded_mission_id, root);
2514
+ if (seeded && TERMINAL_STATUSES.has(String(seeded.status || '')) && seeded.continued_by_mission_id) {
2515
+ return {
2516
+ inserted: false,
2517
+ reason: 'already_continued',
2518
+ mission_id: parent.continuation_seeded_mission_id,
2519
+ mission: null,
2520
+ continued_by_mission_id: seeded.continued_by_mission_id,
2521
+ };
2522
+ }
2523
+ return {
2524
+ inserted: false,
2525
+ reason: 'already_seeded',
2526
+ mission_id: parent.continuation_seeded_mission_id,
2527
+ mission: seeded || null,
2528
+ };
2529
+ }
1001
2530
 
1002
2531
  const existing = findActiveContinuationMission(parent, root);
1003
2532
  if (existing) return { inserted: false, reason: 'active_continuation_exists', mission: existing };
1004
2533
 
1005
2534
  const owner = parent.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
1006
2535
  const objective = continuationObjective(parent);
2536
+ const parentRunner = String(parent.runner || '').trim().toLowerCase();
2537
+ const continuationRunner = parentRunner === 'atris2' ? 'atris2' : 'codex_goal';
1007
2538
  const mission = missionFromArgs([
1008
2539
  objective,
1009
2540
  '--owner',
1010
2541
  owner,
1011
2542
  '--runner',
1012
- 'codex_goal',
2543
+ continuationRunner,
1013
2544
  '--lane',
1014
2545
  parent.lane || 'workspace',
1015
2546
  '--cadence',
@@ -1025,8 +2556,11 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
1025
2556
  continue_on_complete: false,
1026
2557
  continuation_policy: 'choose_next_mission',
1027
2558
  parent_proof: proof || parent.receipt_path || null,
1028
- next_action: `decide next mission, then run: atris mission run "<next useful mission>" --owner ${owner}`,
2559
+ next_action: '',
1029
2560
  };
2561
+ const nextPlan = chooseNextMissionPlan(nextMission, root);
2562
+ nextMission.next_action = `decide next mission, then run: ${nextPlan.command}`;
2563
+ nextMission.next_action_preview = nextPlan.preview;
1030
2564
 
1031
2565
  ensureMemberMissionFile(nextMission.owner, root, nextMission.objective);
1032
2566
  const { mission: saved } = saveMission(nextMission, root, 'mission_continuation_started', {
@@ -1057,6 +2591,36 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
1057
2591
  };
1058
2592
  }
1059
2593
 
2594
+ function seedNextMoveContinuationGoal(root = process.cwd()) {
2595
+ const candidates = listMissions(root)
2596
+ .filter((mission) => runnerUsesCallerSession(mission.runner))
2597
+ .filter((mission) => mission.continue_on_complete === true)
2598
+ .filter((mission) => missionCanSeedContinuation(mission));
2599
+ candidates.sort((a, b) => missionSortTime(b) - missionSortTime(a));
2600
+ for (const parent of candidates) {
2601
+ const seeded = seedMissionRunContinuation(parent, root, parent.receipt_path || 'agent-certified task waiting for human accept');
2602
+ if (seeded?.mission && missionSelectableForCodexGoal(seeded.mission)) return { ...seeded, parent: seeded.parent || parent };
2603
+ }
2604
+ return null;
2605
+ }
2606
+
2607
+ // Continuation runs started from inside an existing agent worktree must build
2608
+ // on that work, not restart clean from origin/master. Returns the current HEAD
2609
+ // sha (a sha survives normalizeTargetRef; an unpushed branch name would be
2610
+ // rewritten to a nonexistent origin/ ref) when cwd is an agent worktree.
2611
+ function inheritedWorktreeBase(cwd) {
2612
+ try {
2613
+ const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
2614
+ const root = String(top.stdout || '').trim();
2615
+ if (top.status !== 0 || !root) return '';
2616
+ if (!fs.existsSync(path.join(root, '.atris', 'agent-worktree.json'))) return '';
2617
+ const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
2618
+ return head.status === 0 ? String(head.stdout || '').trim() : '';
2619
+ } catch {
2620
+ return '';
2621
+ }
2622
+ }
2623
+
1060
2624
  function startMission(args) {
1061
2625
  const asJson = wantsJson(args);
1062
2626
  const mission = missionFromArgs(args);
@@ -1068,7 +2632,8 @@ function startMission(args) {
1068
2632
  let created;
1069
2633
  try {
1070
2634
  const { createAgentWorktree } = require('./worktree');
1071
- created = createAgentWorktree({ member: mission.owner, task: mission.objective });
2635
+ const base = readFlag(args, '--base', '') || inheritedWorktreeBase(process.cwd());
2636
+ created = createAgentWorktree({ member: mission.owner, task: mission.objective, ...(base ? { base } : {}) });
1072
2637
  } catch (e) {
1073
2638
  exitMissionError(`[mission start] worktree creation failed: ${e.message}`, 2, asJson);
1074
2639
  }
@@ -1112,35 +2677,88 @@ function startMission(args) {
1112
2677
 
1113
2678
  function startMissionFromRunObjective(objective, args) {
1114
2679
  const asJson = wantsJson(args);
1115
- const verifier = readFlag(args, '--verify', inferRunObjectiveVerifier(objective));
2680
+ const rawObjective = String(objective || '').trim();
2681
+ const inferredLoop = inferRunObjectiveLoopOptions(rawObjective, args);
2682
+ const budgetContract = inferRunObjectiveBudgetContract(rawObjective, args);
2683
+ const inferredOwner = inferredLoop.wantsLongRun && /\bself[-\s]?improve\b/i.test(rawObjective)
2684
+ ? 'auto-improver'
2685
+ : (process.env.ATRIS_AGENT_ID || 'mission-lead');
2686
+ const preflightOwner = readFlag(args, '--owner', inferredOwner);
2687
+ const missionRunPreflight = buildMissionRunRoomPreflight(rawObjective, args, {
2688
+ root: process.cwd(),
2689
+ owner: preflightOwner,
2690
+ allowTrustedRun: !inferredLoop.wantsLongRun,
2691
+ });
2692
+ const missionObjective = missionRunPreflight?.shaped_objective || rawObjective;
2693
+ const verifier = readFlag(
2694
+ args,
2695
+ '--verify',
2696
+ inferRunObjectiveVerifier(missionObjective)
2697
+ || inferRunObjectiveVerifier(rawObjective)
2698
+ || (missionRunPreflight?.trusted_run ? DEFAULT_LONG_RUN_VERIFIER : '')
2699
+ || (inferredLoop.wantsLongRun ? DEFAULT_LONG_RUN_VERIFIER : ''),
2700
+ );
1116
2701
  const stopCondition = readFlag(
1117
2702
  args,
1118
2703
  '--stop',
1119
- verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready',
2704
+ budgetStopCondition(budgetContract) || (inferredLoop.wantsLongRun
2705
+ ? `run for ${inferredLoop.requestedHours || 'the requested overnight window'} hour${inferredLoop.requestedHours === 1 ? '' : 's'}, or stop when proof is ready`
2706
+ : (verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready')),
1120
2707
  );
1121
2708
  const startArgs = [
1122
- objective,
2709
+ missionObjective,
1123
2710
  '--owner',
1124
- readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead'),
2711
+ readFlag(args, '--owner', inferredOwner),
1125
2712
  '--runner',
1126
2713
  readFlag(args, '--runner', 'codex_goal'),
1127
2714
  '--lane',
1128
2715
  readFlag(args, '--lane', 'workspace'),
1129
2716
  '--cadence',
1130
- readFlag(args, '--cadence', 'manual'),
2717
+ readFlag(args, '--cadence', inferredLoop.wantsLongRun ? inferredLoop.cadence : 'manual'),
1131
2718
  '--stop',
1132
2719
  stopCondition,
1133
2720
  ];
1134
2721
  if (verifier) startArgs.push('--verify', verifier);
1135
2722
  const model = readFlag(args, '--model', '');
1136
2723
  if (model) startArgs.push('--model', model);
1137
- if (hasFlag(args, '--always-on')) startArgs.push('--always-on');
1138
- if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp')) startArgs.push('--xp-task');
2724
+ if (hasFlag(args, '--always-on') || inferredLoop.wantsLongRun) startArgs.push('--always-on');
2725
+ if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp') || missionRunPreflight?.task_spine_required) startArgs.push('--xp-task');
1139
2726
 
1140
2727
  const mission = markMissionRunContinuation(missionFromArgs(startArgs));
2728
+ if (missionGoalChainIntent(rawObjective) || missionGoalChainIntent(missionObjective)) {
2729
+ mission.goal_chain = buildMissionGoalChain(missionObjective || rawObjective);
2730
+ mission.next_action = missionGoalChainNextAction(mission.goal_chain);
2731
+ }
2732
+ if (missionRunPreflight) {
2733
+ mission.mission_run_preflight = missionRunPreflight;
2734
+ mission.raw_objective = rawObjective;
2735
+ }
2736
+ const selectedTargetTask = missionRunSelectedTaskTarget(missionRunPreflight);
2737
+ if (selectedTargetTask) {
2738
+ Object.assign(mission, attachSelectedTargetTaskSpine(mission));
2739
+ }
2740
+ if (budgetContract) {
2741
+ mission.budget_contract = budgetContract;
2742
+ if (budgetContract.requested_seconds) mission.max_wall_seconds = budgetContract.requested_seconds;
2743
+ }
2744
+ if (inferredLoop.wantsLongRun) {
2745
+ mission.overnight_loop = {
2746
+ requested_hours: inferredLoop.requestedHours,
2747
+ cadence: mission.cadence,
2748
+ install_command: inferredLoop.requestedHours
2749
+ ? `atris loop start --overnight --cadence ${mission.cadence} --hours ${inferredLoop.requestedHours}`
2750
+ : `atris loop start --overnight --cadence ${mission.cadence}`,
2751
+ };
2752
+ }
2753
+ if (mission.xp_task_enabled && !selectedTargetTask) {
2754
+ const xpTask = createMissionXpTask(mission, process.cwd(), asJson);
2755
+ mission.xp_task = xpTask;
2756
+ mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
2757
+ }
1141
2758
  const warnings = [missingVerifierWarning(mission)].filter(Boolean);
1142
2759
  ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
1143
2760
  const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
2761
+ const directGoalRequest = writeDirectRunCodexGoalRequest(saved, process.cwd());
1144
2762
  const memberState = renderMemberMissionState(saved.owner);
1145
2763
  const logPath = appendMemberLog(saved.owner, 'Mission started from run', {
1146
2764
  mission: saved.objective,
@@ -1151,12 +2769,26 @@ function startMissionFromRunObjective(objective, args) {
1151
2769
  });
1152
2770
  const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
1153
2771
  const completedContinuationGoal = completeActiveContinuationForStartedMission(saved, process.cwd());
1154
- const codexGoalState = refreshCodexGoalController(process.cwd());
2772
+ const nativeGoalStatus = readFlag(args, '--native-goal-status', readFlag(args, '--visible-goal-status', ''));
2773
+ const nativeGoalObjective = readFlag(args, '--native-goal-objective', readFlag(args, '--visible-goal-objective', ''));
2774
+ const nativeGoalOptions = {
2775
+ ...(nativeGoalStatus ? { nativeGoalStatus } : {}),
2776
+ ...(nativeGoalObjective ? { nativeGoalObjective } : {}),
2777
+ ...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
2778
+ };
2779
+ const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: saved.id });
2780
+ const codexGoalState = runnerUsesCallerSession(saved.runner)
2781
+ ? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
2782
+ : null;
2783
+ const nativeGoal = codexGoalState?.native_goal_action
2784
+ || (codexGoalState?.goal?.requires_native_goal_start ? codexGoalState.goal.native_goal_action : null);
2785
+ const nextCommand = codexGoalState?.next_command || codexGoalState?.goal?.next_command || atrisGoalState.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`;
1155
2786
  printJsonOrText(
1156
2787
  {
1157
2788
  ok: true,
1158
2789
  action: 'mission_run_started',
1159
2790
  mission: saved,
2791
+ budget_contract: saved.budget_contract || null,
1160
2792
  warnings,
1161
2793
  state_path: statePaths().missionsJsonl,
1162
2794
  member_state: memberState,
@@ -1167,16 +2799,16 @@ function startMissionFromRunObjective(objective, args) {
1167
2799
  dirty_hash: worktreeBaseline.dirty_hash,
1168
2800
  } : null,
1169
2801
  completed_continuation_goal: completedContinuationGoal,
2802
+ direct_goal_request: directGoalRequest,
2803
+ atris_goal_state: atrisGoalState,
1170
2804
  codex_goal_state: codexGoalState,
1171
- next_command: codexGoalState.goal?.next_command || `atris mission tick ${saved.id} --verify`,
2805
+ requires_native_goal_start: codexGoalState?.requires_native_goal_start === true || codexGoalState?.goal?.requires_native_goal_start === true,
2806
+ requires_native_goal_replace: codexGoalState?.requires_native_goal_replace === true || codexGoalState?.goal?.requires_native_goal_replace === true,
2807
+ native_goal_action: nativeGoal,
2808
+ native_goal_ack_command: codexGoalState?.goal?.native_goal_ack_command || codexGoalState?.active_goal_conflict?.commands?.ack_new_mission || null,
2809
+ next_command: nextCommand,
1172
2810
  },
1173
- [
1174
- `Started mission: ${saved.objective}`,
1175
- `Owner: ${saved.owner}`,
1176
- `Runner: ${saved.runner}`,
1177
- ...(codexGoalState.goal ? [`Visible goal: ${codexGoalState.goal.objective}`] : []),
1178
- ...warnings.map((warning) => `Warning: ${warning.message}`),
1179
- ],
2811
+ missionRunTakeoffLines(saved, { warnings, nextCommand }),
1180
2812
  asJson,
1181
2813
  );
1182
2814
  }
@@ -1187,60 +2819,128 @@ function attachMissionTask(args) {
1187
2819
  if (!ref) {
1188
2820
  exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
1189
2821
  }
1190
- const mission = resolveMission(ref);
2822
+ let mission = resolveMission(ref);
1191
2823
  if (!mission) {
1192
2824
  exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
1193
2825
  }
1194
- if (TERMINAL_STATUSES.has(mission.status)) {
1195
- exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
2826
+
2827
+ const lock = acquireMissionLock(mission.id, process.cwd(), { waitMs: 2000 });
2828
+ if (!lock.ok) {
2829
+ exitMissionError(`[mission attach-task] lock busy (held by pid ${lock.holder?.pid || '?'} since ${lock.holder?.started_at || '?'}). Exit.`, 3, asJson);
1196
2830
  }
1197
2831
 
1198
- const existingSpine = missionTaskSpine(mission);
1199
- if (existingSpine?.has_task) {
1200
- const view = missionStatusView(mission);
2832
+ try {
2833
+ mission = resolveMission(mission.id) || mission;
2834
+ if (TERMINAL_STATUSES.has(mission.status)) {
2835
+ releaseMissionLock(lock);
2836
+ exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
2837
+ }
2838
+
2839
+ const existingSpine = missionTaskSpine(mission);
2840
+ if (existingSpine?.has_task) {
2841
+ let currentMission = mission;
2842
+ if (missionChoosesNextMission(currentMission)) {
2843
+ const nextPlan = chooseNextMissionPlan(currentMission);
2844
+ const nextAction = `decide next mission, then run: ${nextPlan.command}`;
2845
+ const currentPreviewTitle = currentMission.next_action_preview?.candidate?.title || null;
2846
+ const nextPreviewTitle = nextPlan.preview?.candidate?.title || null;
2847
+ const previewChanged = Boolean(currentMission.next_action_preview) !== Boolean(nextPlan.preview)
2848
+ || currentPreviewTitle !== nextPreviewTitle;
2849
+ if (currentMission.next_action !== nextAction || previewChanged) {
2850
+ currentMission = saveMission(
2851
+ {
2852
+ ...currentMission,
2853
+ next_action: nextAction,
2854
+ next_action_preview: nextPlan.preview,
2855
+ },
2856
+ process.cwd(),
2857
+ 'mission_next_action_refreshed',
2858
+ {
2859
+ next_command: nextPlan.command,
2860
+ target: nextPreviewTitle,
2861
+ },
2862
+ ).mission;
2863
+ }
2864
+ }
2865
+ const view = missionStatusView(currentMission);
2866
+ printJsonOrText(
2867
+ { ok: true, action: 'mission_task_spine_exists', mission: view, task_spine: view.task_spine },
2868
+ [
2869
+ `Mission task spine already exists: ${mission.objective}`,
2870
+ `Task: ${view.task_spine.task_ref}`,
2871
+ `Next: ${view.task_spine.current_step_command}`,
2872
+ ],
2873
+ asJson,
2874
+ );
2875
+ return;
2876
+ }
2877
+
2878
+ const ownership = applyMissionOwnerResolution(mission, process.cwd());
2879
+ const baseMission = ownership.mission;
2880
+ const selectedTargetMission = attachSelectedTargetTaskSpine(baseMission);
2881
+ if (missionTaskSpine(selectedTargetMission)?.has_task) {
2882
+ const { mission: saved } = saveMission(
2883
+ selectedTargetMission,
2884
+ process.cwd(),
2885
+ 'mission_selected_task_spine_attached',
2886
+ {
2887
+ task_id: selectedTargetMission.current_task_id || selectedTargetMission.task_ids?.[0] || null,
2888
+ task_ref: selectedTargetMission.task_ref || null,
2889
+ },
2890
+ );
2891
+ const memberState = renderMemberMissionState(saved.owner);
2892
+ const logPath = appendMemberLog(saved.owner, 'Mission selected task spine attached', {
2893
+ mission: saved.objective,
2894
+ task: saved.task_ref || saved.current_task_id,
2895
+ });
2896
+ const view = missionStatusView(saved);
2897
+ printJsonOrText(
2898
+ { ok: true, action: 'mission_selected_task_spine_attached', mission: view, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
2899
+ [
2900
+ `Attached selected task spine: ${saved.objective}`,
2901
+ `Task: ${view.task_spine.task_ref}`,
2902
+ `Next: ${view.task_spine.current_step_command}`,
2903
+ ],
2904
+ asJson,
2905
+ );
2906
+ return;
2907
+ }
2908
+ const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
2909
+ const nextMission = {
2910
+ ...baseMission,
2911
+ xp_task_enabled: true,
2912
+ xp_task: xpTask,
2913
+ task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
2914
+ };
2915
+ if (nextMission.status === 'ready' && nextMission.receipt_path) {
2916
+ nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
2917
+ } else if (missionChoosesNextMission(nextMission)) {
2918
+ const nextPlan = chooseNextMissionPlan(nextMission);
2919
+ nextMission.next_action = `decide next mission, then run: ${nextPlan.command}`;
2920
+ nextMission.next_action_preview = nextPlan.preview;
2921
+ } else if (!nextMission.verifier && !nextMission.always_on) {
2922
+ nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
2923
+ }
2924
+
2925
+ const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
2926
+ const memberState = renderMemberMissionState(saved.owner);
2927
+ const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
2928
+ mission: saved.objective,
2929
+ task: xpTask.ref,
2930
+ });
2931
+ const view = missionStatusView(saved);
1201
2932
  printJsonOrText(
1202
- { ok: true, action: 'mission_task_spine_exists', mission: view, task_spine: view.task_spine },
2933
+ { ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
1203
2934
  [
1204
- `Mission task spine already exists: ${mission.objective}`,
1205
- `Task: ${view.task_spine.task_ref}`,
2935
+ `Attached task spine: ${saved.objective}`,
2936
+ `Task: ${xpTask.ref}`,
1206
2937
  `Next: ${view.task_spine.current_step_command}`,
1207
2938
  ],
1208
2939
  asJson,
1209
2940
  );
1210
- return;
1211
- }
1212
-
1213
- const ownership = applyMissionOwnerResolution(mission, process.cwd());
1214
- const baseMission = ownership.mission;
1215
- const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
1216
- const nextMission = {
1217
- ...baseMission,
1218
- xp_task_enabled: true,
1219
- xp_task: xpTask,
1220
- task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
1221
- };
1222
- if (nextMission.status === 'ready' && nextMission.receipt_path) {
1223
- nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
1224
- } else if (!nextMission.verifier && !nextMission.always_on) {
1225
- nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
2941
+ } finally {
2942
+ releaseMissionLock(lock);
1226
2943
  }
1227
-
1228
- const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
1229
- const memberState = renderMemberMissionState(saved.owner);
1230
- const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
1231
- mission: saved.objective,
1232
- task: xpTask.ref,
1233
- });
1234
- const view = missionStatusView(saved);
1235
- printJsonOrText(
1236
- { ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
1237
- [
1238
- `Attached task spine: ${saved.objective}`,
1239
- `Task: ${xpTask.ref}`,
1240
- `Next: ${view.task_spine.current_step_command}`,
1241
- ],
1242
- asJson,
1243
- );
1244
2944
  }
1245
2945
 
1246
2946
  function statusMission(args) {
@@ -1288,14 +2988,17 @@ function statusMission(args) {
1288
2988
  ` id: ${mission.id}`,
1289
2989
  ` owner: ${mission.owner}`,
1290
2990
  ...(mission.executed_by ? [` executed_by: ${mission.executed_by}`] : []),
1291
- ` state: ${mission.status}`,
1292
- ...missionHeartbeatLines(mission),
1293
- ...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
1294
- ` next: ${mission.next_action || 'tick or verify'}`,
1295
- ...(mission.task_spine?.task_ref ? [` task: ${mission.task_spine.task_ref}`] : []),
2991
+ ` state: ${mission.status}`,
2992
+ ...missionHeartbeatLines(mission),
2993
+ ...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
2994
+ ` next: ${mission.next_action || 'tick or verify'}`,
2995
+ ...(mission.next_action_preview?.feynman?.what ? [` preview: ${mission.next_action_preview.feynman.what}`] : []),
2996
+ ...missionGoalChainLines(mission),
2997
+ ...(mission.task_spine?.task_ref ? [` task: ${mission.task_spine.task_ref}`] : []),
1296
2998
  ...(mission.task_spine?.current_step_command ? [` task next: ${mission.task_spine.current_step_command}`] : []),
1297
2999
  ...(!mission.task_spine?.has_task && mission.task_spine?.ensure_task_command ? [` task setup: ${mission.task_spine.ensure_task_command}`] : []),
1298
3000
  ...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
3001
+ ...missionStatusLandingLines(mission.last_landing),
1299
3002
  ...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
1300
3003
  ])
1301
3004
  : ['No missions yet. Run: atris mission start "..." --owner <member>'],
@@ -1303,48 +3006,381 @@ function statusMission(args) {
1303
3006
  );
1304
3007
  }
1305
3008
 
1306
- function readMissionReceipt(receiptPath, root = process.cwd()) {
1307
- if (!receiptPath) return null;
1308
- const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
1309
- try {
1310
- return JSON.parse(fs.readFileSync(file, 'utf8'));
1311
- } catch {
1312
- return null;
1313
- }
3009
+ function readMissionReceipt(receiptPath, root = process.cwd()) {
3010
+ if (!receiptPath) return null;
3011
+ const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
3012
+ try {
3013
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
3014
+ } catch {
3015
+ return null;
3016
+ }
3017
+ }
3018
+
3019
+ function firstUsefulLine(text, fallback = '') {
3020
+ return String(text || '')
3021
+ .split(/\r?\n/)
3022
+ .map((line) => line.replace(/^[-*\s#]+/, '').trim())
3023
+ .filter(Boolean)
3024
+ .find((line) => !/^(receipt|summary|final|result)$/i.test(line))
3025
+ || fallback;
3026
+ }
3027
+
3028
+ function missionWorkerLabel(mission) {
3029
+ const runner = String(mission && mission.runner || 'manual').toLowerCase();
3030
+ if (runner === 'atris2') return `Remote Atris2 computer${mission.model ? ` using ${mission.model}` : ''}`;
3031
+ if (runner === 'claude') return `Claude worker${mission.model ? ` using ${mission.model}` : ''}`;
3032
+ if (runner === 'codex_goal') return 'Codex goal handoff';
3033
+ return 'Local mission tick';
3034
+ }
3035
+
3036
+ function missionWorkerSummary(mission, receipt) {
3037
+ if (mission && mission.worker_summary) return mission.worker_summary;
3038
+ const tick = receipt && receipt.result && (receipt.result.tick || (Array.isArray(receipt.result.ticks) ? receipt.result.ticks[receipt.result.ticks.length - 1] : null));
3039
+ if (!tick) return mission && mission.last_tick_reason ? `Last tick: ${mission.last_tick_reason}` : 'No worker receipt yet.';
3040
+ return missionTickReportSummary(tick);
3041
+ }
3042
+
3043
+ function missionTickReportSummary(tick) {
3044
+ if (!tick) return 'Worker tick recorded.';
3045
+ if (tick.atris2) {
3046
+ return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Remote worker ran and returned a response.' : 'Remote worker failed.');
3047
+ }
3048
+ if (tick.claude) {
3049
+ if (tick.claude.skipped) {
3050
+ const skipReason = tick.claude.reason || tick.reason || '';
3051
+ if (!tick.summary && ['runner-uses-caller-session', 'orchestrator-is-caller-session'].includes(skipReason)) {
3052
+ return 'Goal handed to the active Codex session.';
3053
+ }
3054
+ return tick.summary || `Worker step skipped: ${tick.claude.reason || tick.reason || 'not needed'}.`;
3055
+ }
3056
+ return tick.claude.summary || firstUsefulLine(tick.claude.receipt_text, tick.claude.ok ? 'Worker ran and returned a response.' : 'Worker failed.');
3057
+ }
3058
+ if (tick.summary) return tick.summary;
3059
+ return tick.reason ? `Worker tick: ${tick.reason}` : 'Worker tick recorded.';
3060
+ }
3061
+
3062
+ function missionReceiptTicks(receipt) {
3063
+ const result = receipt && receipt.result;
3064
+ if (!result) return [];
3065
+ if (result.tick) return [result.tick];
3066
+ if (Array.isArray(result.ticks)) return result.ticks.filter(Boolean);
3067
+ return [];
3068
+ }
3069
+
3070
+ function missionTickAt(receipt, tick) {
3071
+ return tick.finished_at || tick.started_at || receipt.at || '';
3072
+ }
3073
+
3074
+ function missionTimelineTitle(tick, summary) {
3075
+ const tickIndex = Number(tick && tick.tick_index);
3076
+ const prefix = Number.isInteger(tickIndex) && tickIndex > 0 ? `Goal ${tickIndex}` : 'Goal';
3077
+ const text = String(summary || '').trim();
3078
+ const explicitGoal = text.match(/^(Goal\s+\d+)\s*:\s*(.*)$/i);
3079
+ if (explicitGoal) {
3080
+ return missionTimelineTitleLine(`${explicitGoal[1].replace(/\s+/g, ' ')}: ${explicitGoal[2].trim()}`);
3081
+ }
3082
+ return missionTimelineTitleLine(`${prefix}: ${text.replace(/^Goal\s*:\s*/i, '').trim()}`);
3083
+ }
3084
+
3085
+ function missionTimelineTitleLine(title) {
3086
+ const text = String(title || '').replace(/\s+/g, ' ').trim();
3087
+ const firstSentence = text.match(/^(.+?[.!?])(?:\s+|$)/);
3088
+ const candidate = firstSentence ? firstSentence[1].trim() : text;
3089
+ if (candidate.length <= 180) return candidate;
3090
+ return `${candidate.slice(0, 177).trimEnd()}...`;
3091
+ }
3092
+
3093
+ function missionTimelineSummaryIsUseful(summary) {
3094
+ const text = String(summary || '').trim();
3095
+ if (!text) return false;
3096
+ return ![
3097
+ 'Goal handed to the active Codex session.',
3098
+ 'Worker tick recorded.',
3099
+ ].includes(text) && !/^Worker step skipped:/i.test(text);
3100
+ }
3101
+
3102
+ function missionTimelineTickIndex(tick) {
3103
+ const tickIndex = Number(tick && tick.tick_index);
3104
+ return Number.isInteger(tickIndex) && tickIndex > 0 ? tickIndex : null;
3105
+ }
3106
+
3107
+ function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
3108
+ const paths = statePaths(root);
3109
+ let files = [];
3110
+ try {
3111
+ files = fs.readdirSync(paths.runsDir)
3112
+ .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
3113
+ .map((file) => path.join(paths.runsDir, file));
3114
+ } catch {
3115
+ files = [];
3116
+ }
3117
+
3118
+ const items = [];
3119
+ const seen = new Set();
3120
+ for (const file of files) {
3121
+ let receipt = null;
3122
+ try {
3123
+ receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
3124
+ } catch {
3125
+ continue;
3126
+ }
3127
+ if (!receipt || receipt.mission_id !== mission.id) continue;
3128
+ const receiptPath = path.relative(root, file);
3129
+ for (const tick of missionReceiptTicks(receipt)) {
3130
+ const summary = missionTickReportSummary(tick);
3131
+ if (!missionTimelineSummaryIsUseful(summary)) continue;
3132
+ const at = missionTickAt(receipt, tick);
3133
+ const key = `${tick.tick_index || ''}:${at}:${summary}`;
3134
+ if (seen.has(key)) continue;
3135
+ seen.add(key);
3136
+ items.push({
3137
+ at,
3138
+ tick_index: missionTimelineTickIndex(tick),
3139
+ title: missionTimelineTitle(tick, summary),
3140
+ summary,
3141
+ receipt_path: receiptPath,
3142
+ });
3143
+ }
3144
+ }
3145
+
3146
+ items.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')));
3147
+ return items.slice(Math.max(0, items.length - limit));
3148
+ }
3149
+
3150
+ function missionLandingTimeline(mission, root = process.cwd(), limit = 12) {
3151
+ const paths = statePaths(root);
3152
+ let files = [];
3153
+ try {
3154
+ files = fs.readdirSync(paths.runsDir)
3155
+ .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
3156
+ .map((file) => path.join(paths.runsDir, file));
3157
+ } catch {
3158
+ files = [];
3159
+ }
3160
+
3161
+ const items = [];
3162
+ const seen = new Set();
3163
+ for (const file of files) {
3164
+ let receipt = null;
3165
+ try {
3166
+ receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
3167
+ } catch {
3168
+ continue;
3169
+ }
3170
+ if (!receipt || receipt.mission_id !== mission.id) continue;
3171
+ const landing = receipt.result && receipt.result.landing;
3172
+ if (landing && landing.timeline_visible === false) continue;
3173
+ const changed = String(landing && landing.changed || '').trim();
3174
+ const next = String(landing && landing.next || '').trim();
3175
+ if (!changed && !next) continue;
3176
+ const receiptPath = path.relative(root, file);
3177
+ const key = `${receipt.at || ''}:${changed}:${next}`;
3178
+ if (seen.has(key)) continue;
3179
+ seen.add(key);
3180
+ items.push({
3181
+ at: receipt.at || '',
3182
+ changed,
3183
+ next,
3184
+ receipt_path: receiptPath,
3185
+ created_next: receipt.result.created_next || null,
3186
+ });
3187
+ }
3188
+ items.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')));
3189
+ const normalizedLimit = Number.isFinite(limit) && limit > 0 ? limit : items.length;
3190
+ const shown = items.slice(Math.max(0, items.length - normalizedLimit));
3191
+ return {
3192
+ items: shown,
3193
+ meta: {
3194
+ shown_count: shown.length,
3195
+ total_count: items.length,
3196
+ hidden_count: Math.max(0, items.length - shown.length),
3197
+ truncated: shown.length < items.length,
3198
+ limit: normalizedLimit === Number.MAX_SAFE_INTEGER ? null : normalizedLimit,
3199
+ },
3200
+ };
3201
+ }
3202
+
3203
+ function formatInteger(value) {
3204
+ return String(Math.max(0, Number(value || 0))).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
3205
+ }
3206
+
3207
+ function missionTimelinePruneSummaryMarkdown(prunePreview) {
3208
+ if (!prunePreview) return [];
3209
+ if (prunePreview.error) {
3210
+ return [
3211
+ '',
3212
+ '## Latest prune dry-run',
3213
+ '',
3214
+ `- Not available: ${prunePreview.error}`,
3215
+ ];
3216
+ }
3217
+ return [
3218
+ '',
3219
+ '## Latest prune dry-run',
3220
+ '',
3221
+ `- Policy: keep newest ${formatInteger(prunePreview.policy?.keep_newest)}; keep ${formatInteger(prunePreview.policy?.keep_days)} days`,
3222
+ `- Total run files: ${formatInteger(prunePreview.total_files)}`,
3223
+ `- Total run bytes: ${formatInteger(prunePreview.total_bytes)} (${formatBytes(prunePreview.total_bytes)})`,
3224
+ `- Referenced files kept: ${formatInteger(prunePreview.referenced_files)}`,
3225
+ `- Recent files kept: ${formatInteger(prunePreview.recent_files)}`,
3226
+ `- Would prune: ${formatInteger(prunePreview.prune_count)} files / ${formatInteger(prunePreview.prune_bytes)} bytes (${formatBytes(prunePreview.prune_bytes)})`,
3227
+ `- Deleted files: ${formatInteger(prunePreview.deleted_count)}`,
3228
+ ];
3229
+ }
3230
+
3231
+ function missionTimelinePruneSummaryLine(prunePreview) {
3232
+ if (!prunePreview) return null;
3233
+ if (prunePreview.error) return `Prune dry-run: not available (${prunePreview.error}).`;
3234
+ return `Prune dry-run: ${formatInteger(prunePreview.prune_count)} files / ${formatBytes(prunePreview.prune_bytes)} would prune; ${formatInteger(prunePreview.deleted_count)} deleted.`;
3235
+ }
3236
+
3237
+ function missionTimelinePruneSummaryObject(prunePreview) {
3238
+ if (!prunePreview) return null;
3239
+ if (prunePreview.error) {
3240
+ return {
3241
+ ok: false,
3242
+ error: prunePreview.error,
3243
+ text: missionTimelinePruneSummaryLine(prunePreview),
3244
+ };
3245
+ }
3246
+ return {
3247
+ ok: true,
3248
+ text: missionTimelinePruneSummaryLine(prunePreview),
3249
+ total_files: prunePreview.total_files,
3250
+ total_bytes: prunePreview.total_bytes,
3251
+ referenced_files: prunePreview.referenced_files,
3252
+ recent_files: prunePreview.recent_files,
3253
+ prune_count: prunePreview.prune_count,
3254
+ prune_bytes: prunePreview.prune_bytes,
3255
+ prune_bytes_text: formatBytes(prunePreview.prune_bytes),
3256
+ deleted_count: prunePreview.deleted_count,
3257
+ };
3258
+ }
3259
+
3260
+ function missionTimelineCurrentLanding(timeline) {
3261
+ if (!timeline.length) return null;
3262
+ const latest = timeline[timeline.length - 1];
3263
+ return {
3264
+ at: latest.at || '',
3265
+ changed: latest.changed || '',
3266
+ next: latest.next || '',
3267
+ receipt_path: latest.receipt_path || '',
3268
+ };
3269
+ }
3270
+
3271
+ function missionTimelineEmptyStateDisplay(mission, timelineLength = 0) {
3272
+ const hasMission = Boolean(mission);
3273
+ const isEmpty = !timelineLength;
3274
+ return {
3275
+ label: 'Empty state',
3276
+ is_empty: isEmpty,
3277
+ has_mission: hasMission,
3278
+ title: isEmpty ? (hasMission ? 'No timeline items yet.' : 'No missions yet.') : null,
3279
+ message: isEmpty
3280
+ ? (hasMission ? 'Run the mission once to create the first timeline item.' : 'Start a mission to create the first timeline item.')
3281
+ : null,
3282
+ action_label: isEmpty ? (hasMission ? 'Run mission' : 'Start mission') : null,
3283
+ command: isEmpty ? (hasMission ? `atris mission run ${mission.id} --create-next` : 'atris mission start "..." --owner <member>') : null,
3284
+ };
3285
+ }
3286
+
3287
+ function missionTimelineNextMove(mission, landing) {
3288
+ const latestNext = landing ? String(landing.next || '').trim() : '';
3289
+ return latestNext || `atris mission run ${mission.id} --create-next`;
1314
3290
  }
1315
3291
 
1316
- function firstUsefulLine(text, fallback = '') {
1317
- return String(text || '')
1318
- .split(/\r?\n/)
1319
- .map((line) => line.replace(/^[-*\s#]+/, '').trim())
1320
- .filter(Boolean)
1321
- .find((line) => !/^(receipt|summary|final|result)$/i.test(line))
1322
- || fallback;
3292
+ function missionTimelineCountLine(meta) {
3293
+ if (!meta) return null;
3294
+ const shown = Number(meta.shown_count || 0);
3295
+ const total = Number(meta.total_count || 0);
3296
+ if (meta.truncated) {
3297
+ return `Showing latest ${formatInteger(shown)} of ${formatInteger(total)} ${total === 1 ? 'item' : 'items'}.`;
3298
+ }
3299
+ return `Showing ${formatInteger(shown)} ${shown === 1 ? 'item' : 'items'}.`;
1323
3300
  }
1324
3301
 
1325
- function missionWorkerLabel(mission) {
1326
- const runner = String(mission && mission.runner || 'manual').toLowerCase();
1327
- if (runner === 'atris2') return `Remote Atris2 computer${mission.model ? ` using ${mission.model}` : ''}`;
1328
- if (runner === 'claude') return `Claude worker${mission.model ? ` using ${mission.model}` : ''}`;
1329
- if (runner === 'codex_goal') return 'Codex goal handoff';
1330
- return 'Local mission tick';
3302
+ function missionTimelineCurrentLandingLines(landing) {
3303
+ if (!landing) return [];
3304
+ return [
3305
+ 'Current landing:',
3306
+ ` Changed: ${landing.changed || 'Landing recorded.'}`,
3307
+ ...(landing.next ? [` Next: ${landing.next}`] : []),
3308
+ ` Proof: ${landing.receipt_path}`,
3309
+ '',
3310
+ ];
1331
3311
  }
1332
3312
 
1333
- function missionWorkerSummary(mission, receipt) {
1334
- if (mission && mission.worker_summary) return mission.worker_summary;
1335
- const tick = receipt && receipt.result && (receipt.result.tick || (Array.isArray(receipt.result.ticks) ? receipt.result.ticks[receipt.result.ticks.length - 1] : null));
1336
- if (!tick) return mission && mission.last_tick_reason ? `Last tick: ${mission.last_tick_reason}` : 'No worker receipt yet.';
1337
- if (tick.atris2) {
1338
- return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Remote worker ran and returned a response.' : 'Remote worker failed.');
3313
+ function missionTimelineMarkdown(mission, timeline, { prunePreview = null, generatedAt = stampIso(), timelineMeta = null } = {}) {
3314
+ const latestLanding = timeline.length ? timeline[timeline.length - 1] : null;
3315
+ const nextMove = missionTimelineNextMove(mission, latestLanding);
3316
+ const lines = [
3317
+ `# Mission timeline: ${mission.objective}`,
3318
+ '',
3319
+ `Mission: ${mission.id}`,
3320
+ `Status: ${mission.status}`,
3321
+ `Generated at: ${generatedAt}`,
3322
+ ...(timelineMeta ? [missionTimelineCountLine(timelineMeta)] : []),
3323
+ '',
3324
+ '## Operator commands',
3325
+ '',
3326
+ `- Timeline: \`${missionRunTimelineCommand(mission)}\``,
3327
+ `- Export: \`${missionRunExportCommand(mission)}\``,
3328
+ `- Prune preview: \`${missionRunPrunePreviewCommand(mission)}\``,
3329
+ '',
3330
+ ];
3331
+ if (latestLanding) {
3332
+ lines.push(
3333
+ '## Current landing',
3334
+ '',
3335
+ `Changed: ${latestLanding.changed || 'Landing recorded.'}`,
3336
+ ...(latestLanding.next ? [`Next: ${latestLanding.next}`] : []),
3337
+ `Proof: ${latestLanding.receipt_path}`,
3338
+ '',
3339
+ );
1339
3340
  }
1340
- if (tick.claude) {
1341
- if (tick.claude.skipped) {
1342
- return tick.summary || `Worker step skipped: ${tick.claude.reason || tick.reason || 'not needed'}.`;
1343
- }
1344
- return tick.claude.summary || firstUsefulLine(tick.claude.receipt_text, tick.claude.ok ? 'Worker ran and returned a response.' : 'Worker failed.');
3341
+ if (!timeline.length) {
3342
+ lines.push('No landing receipts yet.');
3343
+ lines.push(
3344
+ '',
3345
+ '## Next move',
3346
+ '',
3347
+ nextMove,
3348
+ );
3349
+ lines.push(
3350
+ '',
3351
+ '## Keep it concise',
3352
+ '',
3353
+ '- Dry run: `atris mission prune-runs --days 14 --keep-newest 200`',
3354
+ '- Apply only after review: add `--apply`.',
3355
+ );
3356
+ lines.push(...missionTimelinePruneSummaryMarkdown(prunePreview));
3357
+ lines.push('');
3358
+ return lines.join('\n');
1345
3359
  }
1346
- if (tick.summary) return tick.summary;
1347
- return tick.reason ? `Worker tick: ${tick.reason}` : 'Worker tick recorded.';
3360
+ timeline.forEach((item, index) => {
3361
+ if (index === 0) lines.push('## Full history', '');
3362
+ lines.push(`${index + 1}. ${item.changed || 'Landing recorded.'}`);
3363
+ if (item.next) lines.push(` - Next: ${item.next}`);
3364
+ lines.push(` - Proof: ${item.receipt_path}`);
3365
+ });
3366
+ lines.push('', '## Next move', '', nextMove);
3367
+ lines.push(
3368
+ '',
3369
+ '## Keep it concise',
3370
+ '',
3371
+ '- Dry run: `atris mission prune-runs --days 14 --keep-newest 200`',
3372
+ '- Apply only after review: add `--apply`.',
3373
+ );
3374
+ lines.push(...missionTimelinePruneSummaryMarkdown(prunePreview));
3375
+ lines.push('');
3376
+ return lines.join('\n');
3377
+ }
3378
+
3379
+ function defaultMissionTimelinePath(root, mission) {
3380
+ const safeId = String(mission.id || 'mission')
3381
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
3382
+ .replace(/^-+|-+$/g, '') || 'mission';
3383
+ return path.join(root, 'atris', 'reports', `${safeId}-timeline.md`);
1348
3384
  }
1349
3385
 
1350
3386
  function missionReportFor(mission, root = process.cwd()) {
@@ -1361,12 +3397,19 @@ function missionReportFor(mission, root = process.cwd()) {
1361
3397
  operator_outcome: operatorOutcome,
1362
3398
  worker: mission.worker || missionWorkerLabel(mission),
1363
3399
  worker_summary: missionWorkerSummary(mission, receipt),
3400
+ timeline: missionReportTimeline(mission, root),
1364
3401
  worker_receipt_path: workerReceiptPath,
1365
3402
  verifier_receipt_path: verifierReceiptPath,
1366
3403
  operator_next: mission.operator_next || mission.next_action || 'Review the mission state.',
1367
3404
  };
1368
3405
  }
1369
3406
 
3407
+ function missionReportNextText(nextAction) {
3408
+ return String(nextAction || 'Review the mission state.')
3409
+ .replace(/^next move:\s*run\s+/i, '')
3410
+ .trim();
3411
+ }
3412
+
1370
3413
  function reportMission(args) {
1371
3414
  const asJson = wantsJson(args);
1372
3415
  const localOnly = hasFlag(args, '--local');
@@ -1396,15 +3439,483 @@ function reportMission(args) {
1396
3439
  ` What happened: ${report.operator_outcome}`,
1397
3440
  ` Worker: ${report.worker}`,
1398
3441
  ` Worker summary: ${report.worker_summary}`,
3442
+ ...(report.timeline && report.timeline.length ? [
3443
+ ' Timeline:',
3444
+ ...report.timeline.map((item) => ` - ${item.title}`),
3445
+ ] : []),
1399
3446
  ...(report.worker_receipt_path ? [` Worker receipt: ${report.worker_receipt_path}`] : []),
1400
3447
  ...(report.verifier_receipt_path ? [` Verifier receipt: ${report.verifier_receipt_path}`] : []),
1401
- ` Next: ${report.operator_next}`,
3448
+ ` Next: ${missionReportNextText(report.operator_next)}`,
1402
3449
  ])
1403
3450
  : ['No missions yet. Run: atris mission start "..." --owner <member>'],
1404
3451
  asJson,
1405
3452
  );
1406
3453
  }
1407
3454
 
3455
+ function timelineMission(args) {
3456
+ const asJson = wantsJson(args);
3457
+ const write = hasFlag(args, '--write');
3458
+ const all = hasFlag(args, '--all');
3459
+ const prunePreviewRequested = hasFlag(args, '--prune-preview');
3460
+ const outputPath = readFlag(args, '--output', '') || readFlag(args, '--out', '');
3461
+ const ref = stripKnownFlags(args, ['--limit', '--output', '--out'], ['--json', '--write', '--all', '--prune-preview'])[0] || '';
3462
+ const limit = all ? Number.MAX_SAFE_INTEGER : readPositiveIntegerFlag(args, '--limit', 12, { json: asJson });
3463
+ const missions = listMissions();
3464
+ const mission = ref
3465
+ ? resolveMission(ref)
3466
+ : (missions.find((row) => !TERMINAL_STATUSES.has(row.status)) || missions[0] || null);
3467
+ if (ref && !mission) {
3468
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
3469
+ }
3470
+ if (!mission) {
3471
+ printJsonOrText(
3472
+ {
3473
+ ok: true,
3474
+ action: 'mission_timeline',
3475
+ mission: null,
3476
+ timeline: [],
3477
+ empty_state_display: missionTimelineEmptyStateDisplay(null, 0),
3478
+ },
3479
+ ['No missions yet. Run: atris mission start "..." --owner <member>'],
3480
+ asJson,
3481
+ );
3482
+ return;
3483
+ }
3484
+ const root = mission.worktree_root || process.cwd();
3485
+ const timelineResult = missionLandingTimeline(mission, root, limit);
3486
+ const timeline = timelineResult.items;
3487
+ const currentLanding = missionTimelineCurrentLanding(timeline);
3488
+ const timelineItemDisplay = (item) => ({
3489
+ changed_label: 'Changed',
3490
+ changed: item.changed || 'Landing recorded.',
3491
+ next_label: 'Next',
3492
+ next: item.next || null,
3493
+ proof_label: 'Proof',
3494
+ proof: item.receipt_path || null,
3495
+ });
3496
+ const currentLandingDisplay = currentLanding ? {
3497
+ label: 'Current landing',
3498
+ ...timelineItemDisplay(currentLanding),
3499
+ } : null;
3500
+ const historyWithoutCurrent = currentLanding ? timeline.slice(0, -1) : timeline;
3501
+ const historyWithoutCurrentDisplay = historyWithoutCurrent.map((item, index) => ({
3502
+ index: index + 1,
3503
+ label: `History item ${index + 1}`,
3504
+ ...timelineItemDisplay(item),
3505
+ }));
3506
+ const timelineDisplay = timeline.map((item, index) => ({
3507
+ index: index + 1,
3508
+ label: `Timeline item ${index + 1}`,
3509
+ at_label: 'When',
3510
+ at: item.at || null,
3511
+ ...timelineItemDisplay(item),
3512
+ }));
3513
+ const nextMove = missionTimelineNextMove(mission, currentLanding);
3514
+ const generatedAt = stampIso();
3515
+ let artifactPath = null;
3516
+ let prunePreview = null;
3517
+ if (write || prunePreviewRequested) {
3518
+ try {
3519
+ prunePreview = pruneRuns(root, { keepNewest: 200, keepDays: 14 });
3520
+ } catch (error) {
3521
+ prunePreview = { error: error.message || String(error) };
3522
+ }
3523
+ }
3524
+ if (write) {
3525
+ const outPath = outputPath
3526
+ ? path.resolve(root, outputPath)
3527
+ : defaultMissionTimelinePath(root, mission);
3528
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
3529
+ fs.writeFileSync(outPath, missionTimelineMarkdown(mission, timeline, { prunePreview, generatedAt, timelineMeta: timelineResult.meta }), 'utf8');
3530
+ artifactPath = path.relative(root, outPath);
3531
+ }
3532
+ const pruneSummary = missionTimelinePruneSummaryObject(prunePreview);
3533
+ const pruneDisplay = {
3534
+ label: 'Prune preview',
3535
+ command: missionRunPrunePreviewCommand(mission),
3536
+ available: Boolean(pruneSummary),
3537
+ ok: pruneSummary?.ok ?? null,
3538
+ summary: pruneSummary?.text || null,
3539
+ would_prune_label: 'Would prune',
3540
+ prune_count: pruneSummary?.prune_count ?? null,
3541
+ prune_bytes_text: pruneSummary?.prune_bytes_text || null,
3542
+ deleted_label: 'Deleted',
3543
+ deleted_count: pruneSummary?.deleted_count ?? null,
3544
+ };
3545
+ const payload = {
3546
+ ok: true,
3547
+ action: 'mission_timeline',
3548
+ generated_at: generatedAt,
3549
+ generated: {
3550
+ label: 'Generated at',
3551
+ at: generatedAt,
3552
+ },
3553
+ schema_display: {
3554
+ label: 'Schema',
3555
+ name: 'atris.mission_timeline',
3556
+ version: 1,
3557
+ primary_objects: [
3558
+ 'display',
3559
+ 'summary_display',
3560
+ 'navigation_display',
3561
+ 'filter_display',
3562
+ 'mission_display',
3563
+ 'current_landing_display',
3564
+ 'history_without_current_display',
3565
+ 'timeline_display',
3566
+ 'timeline_meta_display',
3567
+ 'empty_state_display',
3568
+ 'status_display',
3569
+ 'actions_display',
3570
+ 'proof_display',
3571
+ 'receipt_display',
3572
+ 'export_display',
3573
+ 'prune_display',
3574
+ 'artifact_display',
3575
+ ],
3576
+ },
3577
+ summary_display: {
3578
+ label: 'Summary',
3579
+ title: `Mission timeline: ${mission.objective}`,
3580
+ count: missionTimelineCountLine(timelineResult.meta),
3581
+ latest_label: 'Latest',
3582
+ latest: currentLanding?.changed || null,
3583
+ proof_label: 'Proof',
3584
+ proof: currentLanding?.receipt_path || null,
3585
+ next_label: 'Next',
3586
+ next: nextMove,
3587
+ },
3588
+ display: {
3589
+ title: `Mission timeline: ${mission.objective}`,
3590
+ generated: `Generated at: ${generatedAt}`,
3591
+ count: missionTimelineCountLine(timelineResult.meta),
3592
+ current_landing_label: 'Current landing',
3593
+ history_label: 'History',
3594
+ next_label: 'Next',
3595
+ },
3596
+ status_display: {
3597
+ label: 'Status',
3598
+ mission_status_label: 'Mission status',
3599
+ mission_status: mission.status,
3600
+ history_status_label: 'History status',
3601
+ history_status: timelineResult.meta.truncated ? 'Compact history' : 'Full history',
3602
+ count: missionTimelineCountLine(timelineResult.meta),
3603
+ truncated: Boolean(timelineResult.meta.truncated),
3604
+ hidden_count: timelineResult.meta.hidden_count,
3605
+ },
3606
+ empty_state_display: missionTimelineEmptyStateDisplay(mission, timeline.length),
3607
+ mission: {
3608
+ id: mission.id,
3609
+ objective: mission.objective,
3610
+ status: mission.status,
3611
+ },
3612
+ mission_labels: {
3613
+ mission: 'Mission',
3614
+ objective: 'Objective',
3615
+ status: 'Status',
3616
+ },
3617
+ mission_display: {
3618
+ label: 'Mission',
3619
+ title: mission.objective,
3620
+ id: mission.id,
3621
+ status: mission.status,
3622
+ },
3623
+ operator_commands: {
3624
+ timeline: missionRunTimelineCommand(mission),
3625
+ export: missionRunExportCommand(mission),
3626
+ prune_preview: missionRunPrunePreviewCommand(mission),
3627
+ },
3628
+ commands: {
3629
+ timeline: missionRunTimelineCommand(mission),
3630
+ export: missionRunExportCommand(mission),
3631
+ prune_preview: missionRunPrunePreviewCommand(mission),
3632
+ },
3633
+ actions_display: {
3634
+ label: 'Actions',
3635
+ items: [
3636
+ { label: 'Timeline', command: missionRunTimelineCommand(mission) },
3637
+ { label: 'Export', command: missionRunExportCommand(mission) },
3638
+ { label: 'Prune preview', command: missionRunPrunePreviewCommand(mission) },
3639
+ ],
3640
+ },
3641
+ navigation_display: {
3642
+ label: 'Navigation',
3643
+ current_label: 'Current view',
3644
+ current: 'timeline',
3645
+ items: [
3646
+ { key: 'timeline', label: 'Timeline', command: missionRunTimelineCommand(mission), active: true },
3647
+ { key: 'export', label: 'Full history', command: missionRunExportCommand(mission), active: false },
3648
+ { key: 'prune_preview', label: 'Prune preview', command: missionRunPrunePreviewCommand(mission), active: false },
3649
+ ],
3650
+ },
3651
+ filter_display: {
3652
+ label: 'Filters',
3653
+ active_label: 'Active filter',
3654
+ active: all ? 'full_history' : 'latest',
3655
+ limit_label: 'Limit',
3656
+ limit: timelineResult.meta.limit,
3657
+ shown_count: timelineResult.meta.shown_count,
3658
+ total_count: timelineResult.meta.total_count,
3659
+ hidden_count: timelineResult.meta.hidden_count,
3660
+ truncated_label: 'Truncated',
3661
+ truncated: Boolean(timelineResult.meta.truncated),
3662
+ items: [
3663
+ { key: 'latest', label: 'Latest', command: missionRunTimelineCommand(mission), active: !all },
3664
+ { key: 'full_history', label: 'Full history', command: `atris mission timeline ${mission.id} --all`, active: Boolean(all) },
3665
+ ],
3666
+ },
3667
+ current_landing: currentLanding,
3668
+ current_landing_display: currentLandingDisplay,
3669
+ current_landing_label: 'Current landing',
3670
+ history_without_current: historyWithoutCurrent,
3671
+ history_without_current_display: historyWithoutCurrentDisplay,
3672
+ history_without_current_count: historyWithoutCurrent.length,
3673
+ has_history_without_current: historyWithoutCurrent.length > 0,
3674
+ history_label: 'History',
3675
+ labels: {
3676
+ current_landing: 'Current landing',
3677
+ history: 'History',
3678
+ },
3679
+ counts: {
3680
+ timeline: timeline.length,
3681
+ history_without_current: historyWithoutCurrent.length,
3682
+ total: timelineResult.meta.total_count,
3683
+ hidden: timelineResult.meta.hidden_count,
3684
+ shown: timelineResult.meta.shown_count,
3685
+ },
3686
+ booleans: {
3687
+ has_current_landing: Boolean(currentLanding),
3688
+ has_history_without_current: historyWithoutCurrent.length > 0,
3689
+ truncated: Boolean(timelineResult.meta.truncated),
3690
+ all: Boolean(all),
3691
+ },
3692
+ next_move: nextMove,
3693
+ next: {
3694
+ label: 'Next',
3695
+ move: nextMove,
3696
+ has_move: Boolean(nextMove),
3697
+ },
3698
+ timeline,
3699
+ timeline_display: timelineDisplay,
3700
+ timeline_meta: timelineResult.meta,
3701
+ timeline_meta_display: {
3702
+ label: 'Timeline metadata',
3703
+ shown_label: 'Shown',
3704
+ shown_count: timelineResult.meta.shown_count,
3705
+ total_label: 'Total',
3706
+ total_count: timelineResult.meta.total_count,
3707
+ hidden_label: 'Hidden',
3708
+ hidden_count: timelineResult.meta.hidden_count,
3709
+ limit_label: 'Limit',
3710
+ limit: timelineResult.meta.limit,
3711
+ truncated_label: 'Truncated',
3712
+ truncated: Boolean(timelineResult.meta.truncated),
3713
+ },
3714
+ all,
3715
+ prune_summary: pruneSummary,
3716
+ prune_display: pruneDisplay,
3717
+ prune_preview: prunePreview,
3718
+ artifact: {
3719
+ path: artifactPath,
3720
+ written: Boolean(artifactPath),
3721
+ format: artifactPath ? 'markdown' : null,
3722
+ },
3723
+ artifact_display: {
3724
+ label: 'Artifact',
3725
+ path_label: 'Path',
3726
+ path: artifactPath,
3727
+ written_label: 'Written',
3728
+ written: Boolean(artifactPath),
3729
+ format_label: 'Format',
3730
+ format: artifactPath ? 'markdown' : null,
3731
+ },
3732
+ export_display: {
3733
+ label: 'Export',
3734
+ command: missionRunExportCommand(mission),
3735
+ report_label: 'Saved report',
3736
+ report_path: artifactPath,
3737
+ report_written: Boolean(artifactPath),
3738
+ report_format: artifactPath ? 'markdown' : null,
3739
+ },
3740
+ receipt_display: {
3741
+ label: 'Receipts',
3742
+ latest_label: 'Latest receipt',
3743
+ latest_path: currentLanding?.receipt_path || null,
3744
+ has_latest: Boolean(currentLanding?.receipt_path),
3745
+ count_label: 'Receipts',
3746
+ count: timeline.filter((item) => item.receipt_path).length,
3747
+ items: timeline.map((item, index) => ({
3748
+ index: index + 1,
3749
+ label: `Receipt ${index + 1}`,
3750
+ path: item.receipt_path || null,
3751
+ at: item.at || null,
3752
+ current: Boolean(currentLanding?.receipt_path && item.receipt_path === currentLanding.receipt_path),
3753
+ })),
3754
+ },
3755
+ proof_display: {
3756
+ label: 'Proof',
3757
+ latest_receipt_label: 'Latest receipt',
3758
+ latest_receipt_path: currentLanding?.receipt_path || null,
3759
+ report_label: 'Saved report',
3760
+ report_path: artifactPath,
3761
+ report_written: Boolean(artifactPath),
3762
+ report_format: artifactPath ? 'markdown' : null,
3763
+ },
3764
+ artifact_path: artifactPath,
3765
+ };
3766
+ const lines = timeline.length
3767
+ ? [
3768
+ `Mission timeline: ${mission.objective}`,
3769
+ `Generated at: ${generatedAt}`,
3770
+ missionTimelineCountLine(timelineResult.meta),
3771
+ ...missionTimelineCurrentLandingLines(currentLanding),
3772
+ ...(historyWithoutCurrent.length ? ['History:'] : []),
3773
+ ...historyWithoutCurrent.flatMap((item, index) => [
3774
+ ` ${index + 1}. ${item.changed || 'Landing recorded.'}`,
3775
+ ...(item.next ? [` Next: ${item.next}`] : []),
3776
+ ` Proof: ${item.receipt_path}`,
3777
+ ]),
3778
+ ]
3779
+ : [
3780
+ `Mission timeline: ${mission.objective}`,
3781
+ `Generated at: ${generatedAt}`,
3782
+ missionTimelineCountLine(timelineResult.meta),
3783
+ ' No landing receipts yet.',
3784
+ ` Next: atris mission run ${mission.id} --create-next`,
3785
+ ];
3786
+ if (timelineResult.meta.truncated) lines.push(`Full history: ${missionRunExportCommand(mission)}`);
3787
+ if (artifactPath) lines.push(`Saved: ${artifactPath}`);
3788
+ const pruneLine = missionTimelinePruneSummaryLine(prunePreview);
3789
+ if (pruneLine) lines.push(pruneLine);
3790
+ printJsonOrText(payload, lines, asJson);
3791
+ }
3792
+
3793
+ function missionReceiptHealth(mission, root = process.cwd()) {
3794
+ const receiptPath = String(mission?.receipt_path || '').trim();
3795
+ if (!receiptPath) return { ok: false, reason: 'missing_receipt_path' };
3796
+ const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
3797
+ let receipt = null;
3798
+ try {
3799
+ receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
3800
+ } catch {
3801
+ return { ok: false, reason: 'receipt_not_found', receipt_path: receiptPath };
3802
+ }
3803
+ const missionVerifierPassedState = mission?.verifier_result?.passed === true;
3804
+ const receiptPassed = receipt?.result?.passed === true
3805
+ || receipt?.result?.verifier_result?.passed === true;
3806
+ if (!missionVerifierPassedState || !receiptPassed) {
3807
+ return { ok: false, reason: 'verifier_not_passed', receipt_path: receiptPath };
3808
+ }
3809
+ return { ok: true, reason: 'verifier_passed', receipt_path: receiptPath };
3810
+ }
3811
+
3812
+ function collectMissionDoctorFindings(root = process.cwd(), options = {}) {
3813
+ const localOnly = options.localOnly === true;
3814
+ let missions = listMissions(root);
3815
+ if (!localOnly) {
3816
+ const seen = new Set(missions.map((mission) => mission.id));
3817
+ for (const rolled of listWorktreeRollupMissions(root)) {
3818
+ if (seen.has(rolled.id)) continue;
3819
+ seen.add(rolled.id);
3820
+ missions.push(rolled);
3821
+ }
3822
+ }
3823
+ missions = missions.map(missionStatusView);
3824
+ const findings = [];
3825
+ const add = (mission, code, message, severity = 'high', extra = {}) => {
3826
+ findings.push({
3827
+ severity,
3828
+ code,
3829
+ mission_id: mission.id,
3830
+ owner: mission.owner,
3831
+ status: mission.status,
3832
+ objective: mission.objective,
3833
+ message,
3834
+ next: extra.next || `atris mission status ${mission.id}`,
3835
+ ...extra,
3836
+ });
3837
+ };
3838
+
3839
+ for (const mission of missions) {
3840
+ const status = String(mission.status || '');
3841
+ const active = !TERMINAL_STATUSES.has(status);
3842
+ const objective = String(mission.objective || '').trim();
3843
+ if (active && !effectiveMissionVerifier(mission)) {
3844
+ add(
3845
+ mission,
3846
+ 'missing_verifier',
3847
+ 'Mission has no verifier, so it cannot prove done work.',
3848
+ 'high',
3849
+ { next: `atris mission start "${objective}" --owner ${mission.owner} --verify "<cmd>"` },
3850
+ );
3851
+ }
3852
+ if (active && /^(?:--)?help$/i.test(objective)) {
3853
+ add(
3854
+ mission,
3855
+ 'accidental_help_mission',
3856
+ 'Looks like a help flag became a mission.',
3857
+ 'medium',
3858
+ { next: `atris mission stop ${mission.id} --reason "accidental help mission"` },
3859
+ );
3860
+ }
3861
+ if (status === 'ready') {
3862
+ const receiptRoot = mission.worktree_root || root;
3863
+ const health = missionReceiptHealth(mission, receiptRoot);
3864
+ if (!health.ok) {
3865
+ add(
3866
+ mission,
3867
+ 'stale_ready_receipt',
3868
+ `Ready mission does not have a fresh passing receipt (${health.reason}).`,
3869
+ 'high',
3870
+ {
3871
+ receipt_path: health.receipt_path || mission.receipt_path || null,
3872
+ receipt_reason: health.reason,
3873
+ next: `atris mission tick ${mission.id} --verify`,
3874
+ },
3875
+ );
3876
+ }
3877
+ }
3878
+ if (mission.always_on === true && status === 'blocked') {
3879
+ add(
3880
+ mission,
3881
+ 'blocked_always_on_loop',
3882
+ 'Always-on mission is blocked and will not keep improving.',
3883
+ 'high',
3884
+ { next: `atris mission run ${mission.id} --max-ticks 1` },
3885
+ );
3886
+ }
3887
+ }
3888
+
3889
+ findings.sort((a, b) => {
3890
+ const rank = { high: 0, medium: 1, low: 2 };
3891
+ return (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9)
3892
+ || String(a.code).localeCompare(String(b.code))
3893
+ || String(a.objective).localeCompare(String(b.objective));
3894
+ });
3895
+ return { missions, findings };
3896
+ }
3897
+
3898
+ function doctorMission(args) {
3899
+ const asJson = wantsJson(args);
3900
+ const localOnly = hasFlag(args, '--local');
3901
+ const { missions, findings } = collectMissionDoctorFindings(process.cwd(), { localOnly });
3902
+ const payload = {
3903
+ ok: findings.length === 0,
3904
+ action: 'mission_doctor',
3905
+ checked_count: missions.length,
3906
+ finding_count: findings.length,
3907
+ findings,
3908
+ };
3909
+ const lines = findings.length
3910
+ ? [
3911
+ `Mission doctor: ${findings.length} problem(s) across ${missions.length} mission(s)`,
3912
+ ...findings.map((finding) => `${finding.severity.toUpperCase()} ${finding.code}: ${finding.objective} -> ${finding.next}`),
3913
+ ]
3914
+ : [`Mission doctor: clean (${missions.length} mission(s) checked)`];
3915
+ printJsonOrText(payload, lines, asJson);
3916
+ if (findings.length) process.exitCode = 1;
3917
+ }
3918
+
1408
3919
  // `atris mission watch [id]` — read-only live heartbeat. Prints a line per tick as it
1409
3920
  // lands so a human (or any terminal) can see the loop is alive without rerunning status.
1410
3921
  function watchMission(args) {
@@ -1472,11 +3983,8 @@ function writeReceipt(mission, result, root = process.cwd()) {
1472
3983
  fs.mkdirSync(paths.runsDir, { recursive: true });
1473
3984
  const safeTime = stampIso().replace(/[:.]/g, '-');
1474
3985
  const receiptPath = path.join(paths.runsDir, `mission-${mission.id}-${safeTime}.json`);
1475
- // Back-compat: legacy consumers read receipt.result.passed (verifier-only shape).
1476
- // New shape nests verifier under result.verifier_result, so mirror .passed at top.
1477
- const finalResult = (result && typeof result === 'object' && result.verifier_result && !('passed' in result))
1478
- ? { ...result, passed: !!result.verifier_result.passed }
1479
- : result;
3986
+ const relativeReceiptPath = path.relative(root, receiptPath);
3987
+ const finalResult = normalizeMissionReceiptResult(mission, result, relativeReceiptPath);
1480
3988
  fs.writeFileSync(receiptPath, JSON.stringify({
1481
3989
  schema: 'atris.mission_receipt.v1',
1482
3990
  mission_id: mission.id,
@@ -1486,7 +3994,7 @@ function writeReceipt(mission, result, root = process.cwd()) {
1486
3994
  verifier: mission.verifier || null,
1487
3995
  result: finalResult,
1488
3996
  }, null, 2) + '\n', 'utf8');
1489
- return path.relative(root, receiptPath);
3997
+ return relativeReceiptPath;
1490
3998
  }
1491
3999
 
1492
4000
  function shellQuote(value) {
@@ -1721,11 +4229,19 @@ function runnerUsesCallerSession(runner) {
1721
4229
  return new Set(['codex_goal', 'caller_session', 'current_agent']).has(String(runner || '').trim().toLowerCase());
1722
4230
  }
1723
4231
 
4232
+ function isCodexGoalMission(mission) {
4233
+ return String(mission?.runner || '').trim().toLowerCase() === 'codex_goal';
4234
+ }
4235
+
1724
4236
  function nextCandidateTickAction(mission) {
1725
4237
  const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
1726
4238
  return `next move: run atris mission run ${mission.id}${completeFlag}`;
1727
4239
  }
1728
4240
 
4241
+ function nextCandidateRunCommand(mission) {
4242
+ return `atris mission run ${mission.id}`;
4243
+ }
4244
+
1729
4245
  function missionVerifierPassed(mission) {
1730
4246
  return (mission && mission.verifier_result && mission.verifier_result.passed) === true;
1731
4247
  }
@@ -1768,7 +4284,7 @@ function missionHeartbeatLines(mission, now = new Date()) {
1768
4284
  const lastTickAt = mission.last_tick_at ? Date.parse(mission.last_tick_at) : NaN;
1769
4285
  if (Number.isFinite(lastTickAt)) {
1770
4286
  const age = formatDurationShort((now.getTime() - lastTickAt) / 1000);
1771
- const verifier = mission.verifier
4287
+ const verifier = effectiveMissionVerifier(mission)
1772
4288
  ? (mission.verifier_result ? (mission.verifier_result.passed ? 'verifier passed' : 'verifier failed') : 'verifier not run')
1773
4289
  : 'no verifier';
1774
4290
  const tickIdx = mission.last_tick_index != null ? `#${mission.last_tick_index}, ` : '';
@@ -1789,6 +4305,23 @@ function missionHasHumanAsks(mission) {
1789
4305
  && mission.human_asks.some((ask) => String(ask || '').trim());
1790
4306
  }
1791
4307
 
4308
+ function missionTaskHumanAcceptWaiting(mission) {
4309
+ const taskId = missionTaskSpine(mission)?.task_id;
4310
+ if (!taskId) return false;
4311
+ try {
4312
+ const taskDb = require('../lib/task-db');
4313
+ const db = taskDb.open();
4314
+ const task = taskDb.getTask(db, taskId);
4315
+ if (!task) return false;
4316
+ const metadata = task.metadata || {};
4317
+ return task.status === 'review'
4318
+ && metadata.approval_status === 'pending'
4319
+ && metadata.agent_certified === true;
4320
+ } catch {
4321
+ return false;
4322
+ }
4323
+ }
4324
+
1792
4325
  function missionIsRunnable(mission) {
1793
4326
  return mission
1794
4327
  && GOAL_LOOP_STATUSES.has(String(mission.status || ''))
@@ -1802,7 +4335,8 @@ function missionSortTime(mission) {
1802
4335
  function selectDueMission(root = process.cwd(), now = new Date()) {
1803
4336
  const candidates = listMissions(root)
1804
4337
  .filter((mission) => missionSelectableForLoop(mission, now))
1805
- .filter((mission) => mission.verifier)
4338
+ .filter((mission) => !missionTaskHumanAcceptWaiting(mission))
4339
+ .filter((mission) => effectiveMissionVerifier(mission) || callerSessionMissionReadyForDue(mission))
1806
4340
  .filter((mission) => mission.always_on || !missionVerifierPassed(mission))
1807
4341
  .filter((mission) => missionDueAt(mission, now));
1808
4342
 
@@ -1811,82 +4345,423 @@ function selectDueMission(root = process.cwd(), now = new Date()) {
1811
4345
  const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
1812
4346
  if (aCaller !== bCaller) return bCaller - aCaller;
1813
4347
 
1814
- const aTime = missionSortTime(a);
1815
- const bTime = missionSortTime(b);
1816
- return bTime - aTime;
4348
+ const aTime = missionSortTime(a);
4349
+ const bTime = missionSortTime(b);
4350
+ return bTime - aTime;
4351
+ });
4352
+ return candidates[0] || null;
4353
+ }
4354
+
4355
+ function callerSessionMissionReadyForDue(mission) {
4356
+ return runnerUsesCallerSession(mission?.runner)
4357
+ && mission?.always_on === true
4358
+ && Boolean(codexNativeGoalAck(mission))
4359
+ && missionTaskSpine(mission)?.has_task === true;
4360
+ }
4361
+
4362
+ function missionSelectableForCodexGoal(mission, now = new Date()) {
4363
+ if (!missionIsRunnable(mission)) return false;
4364
+ if (missionTaskHumanAcceptWaiting(mission)) return false;
4365
+ if (mission.always_on && missionVerifierPassed(mission) && !missionDueAt(mission, now)) {
4366
+ return parseCadenceSeconds(mission.cadence) > 0;
4367
+ }
4368
+ return true;
4369
+ }
4370
+
4371
+ function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Date()) {
4372
+ const requestedId = String(options.missionId || '').trim();
4373
+ const candidates = listMissions(root)
4374
+ .filter((mission) => runnerUsesCallerSession(mission.runner))
4375
+ .filter((mission) => missionSelectableForCodexGoal(mission, now));
4376
+ if (requestedId) {
4377
+ const exact = candidates.find((mission) => mission.id === requestedId || mission.id.startsWith(requestedId));
4378
+ if (exact) {
4379
+ const due = effectiveMissionVerifier(exact) && missionDueAt(exact, now);
4380
+ return { mission: exact, reason: due ? 'due' : 'selected' };
4381
+ }
4382
+ }
4383
+
4384
+ candidates.sort((a, b) => {
4385
+ const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
4386
+ const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
4387
+ if (aCaller !== bCaller) return bCaller - aCaller;
4388
+
4389
+ const aVerifier = effectiveMissionVerifier(a) ? 1 : 0;
4390
+ const bVerifier = effectiveMissionVerifier(b) ? 1 : 0;
4391
+ if (aVerifier !== bVerifier) return bVerifier - aVerifier;
4392
+
4393
+ const aTime = missionSortTime(a);
4394
+ const bTime = missionSortTime(b);
4395
+ return bTime - aTime;
4396
+ });
4397
+
4398
+ const mission = candidates[0] || null;
4399
+ if (!mission) return null;
4400
+ const due = effectiveMissionVerifier(mission) && missionDueAt(mission, now);
4401
+ return { mission, reason: due ? 'due' : 'active' };
4402
+ }
4403
+
4404
+ function selectAtrisGoalMission(root = process.cwd(), options = {}, now = new Date()) {
4405
+ const requestedId = String(options.missionId || '').trim();
4406
+ const runnable = listMissions(root).filter((mission) => missionIsRunnable(mission));
4407
+ if (requestedId) {
4408
+ const exact = runnable.find((mission) => mission.id === requestedId || mission.id.startsWith(requestedId));
4409
+ if (exact) return { mission: exact, reason: 'selected' };
4410
+ }
4411
+ runnable.sort((a, b) => {
4412
+ const aDue = effectiveMissionVerifier(a) && missionDueAt(a, now) ? 1 : 0;
4413
+ const bDue = effectiveMissionVerifier(b) && missionDueAt(b, now) ? 1 : 0;
4414
+ if (aDue !== bDue) return bDue - aDue;
4415
+
4416
+ const aTime = missionSortTime(a);
4417
+ const bTime = missionSortTime(b);
4418
+ return bTime - aTime;
4419
+ });
4420
+ const mission = runnable[0] || null;
4421
+ if (!mission) return null;
4422
+ const due = effectiveMissionVerifier(mission) && missionDueAt(mission, now);
4423
+ return { mission, reason: due ? 'due' : 'active' };
4424
+ }
4425
+
4426
+ function codexGoalObjective(mission) {
4427
+ return mission.objective;
4428
+ }
4429
+
4430
+ function codexNativeGoalAck(mission, objective = codexGoalObjective(mission)) {
4431
+ const ack = mission?.native_goal_ack || null;
4432
+ if (!ack || String(ack.runtime || '').toLowerCase() !== 'codex') return null;
4433
+ if (ack.status !== 'active') return null;
4434
+ if (String(ack.objective || '') !== String(objective || '')) return null;
4435
+ return ack;
4436
+ }
4437
+
4438
+ function codexRuntimeGoalStateFromOptions(options = {}) {
4439
+ const status = String(options.nativeGoalStatus || options.visibleGoalStatus || '').trim().toLowerCase();
4440
+ const objective = String(options.nativeGoalObjective || options.visibleGoalObjective || '').trim();
4441
+ if (!status && !objective) return null;
4442
+ return {
4443
+ status: status || null,
4444
+ objective: objective || null,
4445
+ };
4446
+ }
4447
+
4448
+ function codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission) {
4449
+ if (!runtimeGoalState) return true;
4450
+ if (runtimeGoalState.status && !new Set(['active', 'paused']).has(runtimeGoalState.status)) return false;
4451
+ if (!runtimeGoalState.objective) return new Set(['active', 'paused']).has(runtimeGoalState.status);
4452
+ return runtimeGoalState.objective === codexGoalObjective(mission);
4453
+ }
4454
+
4455
+ function codexGoalAckCommand(mission, objective = codexGoalObjective(mission)) {
4456
+ return `atris mission goal ack ${mission.id} --runtime codex --status active --objective ${shellQuote(objective)} --json`;
4457
+ }
4458
+
4459
+ function codexNativeGoalReplaceAction(newMission, activeMission, runtimeGoalState = null, commands = {}) {
4460
+ const fromObjective = codexGoalObjective(activeMission);
4461
+ const toObjective = codexGoalObjective(newMission);
4462
+ return {
4463
+ runtime: 'codex',
4464
+ tool: 'replace_goal',
4465
+ available: false,
4466
+ blocked_by: 'codex_runtime_missing_replace_goal_tool',
4467
+ args: {
4468
+ objective: toObjective,
4469
+ from_objective: fromObjective,
4470
+ to_objective: toObjective,
4471
+ from_mission_id: activeMission.id,
4472
+ to_mission_id: newMission.id,
4473
+ current_status: runtimeGoalState?.status || null,
4474
+ },
4475
+ after_success: {
4476
+ ack_new_mission: commands.ack_new_mission || codexGoalAckCommand(newMission, toObjective),
4477
+ },
4478
+ fallback: {
4479
+ reason: 'This Codex runtime exposes get_goal/create_goal/update_goal but not replace_goal or resume_goal.',
4480
+ commands,
4481
+ },
4482
+ };
4483
+ }
4484
+
4485
+ function codexNativeGoalRuntimeReplaceAction(newMission, runtimeGoalState = null, commands = {}) {
4486
+ const toObjective = codexGoalObjective(newMission);
4487
+ const fromObjective = runtimeGoalState?.objective || null;
4488
+ const ackNewMission = commands.ack_new_mission || codexGoalAckCommand(newMission, toObjective);
4489
+ const completeCurrentGoal = 'update_goal({ status: "complete" })';
4490
+ const createNewGoal = `create_goal({ objective: ${JSON.stringify(toObjective)} })`;
4491
+ const supersedeApproved = commands.allow_native_goal_supersede === true;
4492
+ return {
4493
+ runtime: 'codex',
4494
+ tool: 'replace_goal',
4495
+ available: false,
4496
+ blocked_by: 'codex_runtime_missing_replace_goal_tool',
4497
+ args: {
4498
+ objective: toObjective,
4499
+ from_objective: fromObjective,
4500
+ to_objective: toObjective,
4501
+ from_mission_id: null,
4502
+ to_mission_id: newMission.id,
4503
+ current_status: runtimeGoalState?.status || null,
4504
+ },
4505
+ after_success: {
4506
+ ack_new_mission: ackNewMission,
4507
+ },
4508
+ fallback: {
4509
+ reason: 'This Codex runtime exposes get_goal/create_goal/update_goal but not replace_goal or resume_goal.',
4510
+ automatic: supersedeApproved,
4511
+ approved: supersedeApproved,
4512
+ executable_now: supersedeApproved,
4513
+ blocked_by: supersedeApproved ? null : 'native_goal_cancel_or_supersede_tool_missing',
4514
+ safe_when: 'Use only when a mission handoff proves the paused goal is intentionally superseded; update_goal complete otherwise misrepresents abandoned work as finished.',
4515
+ sequence_name: 'complete_paused_goal_then_create_new_goal',
4516
+ sequence: [
4517
+ completeCurrentGoal,
4518
+ createNewGoal,
4519
+ ackNewMission,
4520
+ ],
4521
+ commands: {
4522
+ ...commands,
4523
+ complete_current_goal: completeCurrentGoal,
4524
+ create_new_goal: createNewGoal,
4525
+ ack_new_mission: ackNewMission,
4526
+ },
4527
+ },
4528
+ };
4529
+ }
4530
+
4531
+ function codexRuntimeGoalNeedsReplace(runtimeGoalState, objective) {
4532
+ if (!runtimeGoalState) return false;
4533
+ if (runtimeGoalState.status !== 'paused') return false;
4534
+ if (!runtimeGoalState.objective) return true;
4535
+ return runtimeGoalState.objective !== objective;
4536
+ }
4537
+
4538
+ function writeDirectRunCodexGoalRequest(mission, root = process.cwd()) {
4539
+ if (!isCodexGoalMission(mission)) return null;
4540
+ const request = {
4541
+ schema: 'atris.codex_goal_direct_run_request.v1',
4542
+ source: 'mission_run_objective',
4543
+ mission_id: mission.id,
4544
+ objective: codexGoalObjective(mission),
4545
+ mission_run_preflight: mission.mission_run_preflight || null,
4546
+ requested_at: stampIso(),
4547
+ };
4548
+ const paths = statePaths(root);
4549
+ fs.mkdirSync(path.dirname(paths.codexGoalRequestJson), { recursive: true });
4550
+ fs.writeFileSync(paths.codexGoalRequestJson, JSON.stringify(request, null, 2) + '\n', 'utf8');
4551
+ return request;
4552
+ }
4553
+
4554
+ function readDirectRunCodexGoalRequest(root = process.cwd(), now = new Date()) {
4555
+ const file = statePaths(root).codexGoalRequestJson;
4556
+ let request = null;
4557
+ try {
4558
+ request = JSON.parse(fs.readFileSync(file, 'utf8'));
4559
+ } catch {
4560
+ return null;
4561
+ }
4562
+ const missionId = String(request?.mission_id || '').trim();
4563
+ if (!missionId) return null;
4564
+ const mission = resolveMission(missionId, root);
4565
+ if (!mission || !isCodexGoalMission(mission)) return null;
4566
+ if (!missionSelectableForCodexGoal(mission, now)) return null;
4567
+ return { request, mission };
4568
+ }
4569
+
4570
+ function clearDirectRunCodexGoalRequestForMission(missionId, root = process.cwd()) {
4571
+ const file = statePaths(root).codexGoalRequestJson;
4572
+ let request = null;
4573
+ try {
4574
+ request = JSON.parse(fs.readFileSync(file, 'utf8'));
4575
+ } catch {
4576
+ return false;
4577
+ }
4578
+ if (String(request?.mission_id || '') !== String(missionId || '')) return false;
4579
+ try {
4580
+ fs.rmSync(file, { force: true });
4581
+ return true;
4582
+ } catch {
4583
+ return false;
4584
+ }
4585
+ }
4586
+
4587
+ function activeCodexVisibleGoalOwner(root = process.cwd(), excludeId = '', now = new Date(), runtimeGoalState = null) {
4588
+ const excluded = String(excludeId || '');
4589
+ const candidates = listMissions(root)
4590
+ .filter((mission) => mission.id !== excluded)
4591
+ .filter((mission) => isCodexGoalMission(mission))
4592
+ .filter((mission) => missionSelectableForCodexGoal(mission, now))
4593
+ .filter((mission) => Boolean(codexNativeGoalAck(mission)))
4594
+ .filter((mission) => codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission));
4595
+ candidates.sort((a, b) => {
4596
+ const aAck = Date.parse(a.native_goal_ack?.acknowledged_at || '') || 0;
4597
+ const bAck = Date.parse(b.native_goal_ack?.acknowledged_at || '') || 0;
4598
+ if (aAck !== bAck) return bAck - aAck;
4599
+ return missionSortTime(b) - missionSortTime(a);
1817
4600
  });
1818
4601
  return candidates[0] || null;
1819
4602
  }
1820
4603
 
1821
- function missionSelectableForCodexGoal(mission, now = new Date()) {
1822
- if (!missionIsRunnable(mission)) return false;
1823
- if (mission.always_on && missionVerifierPassed(mission) && !missionDueAt(mission, now)) {
1824
- return parseCadenceSeconds(mission.cadence) > 0;
1825
- }
1826
- return true;
4604
+ function codexGoalActiveConflictPayload(newMission, activeMission, request = null, heartbeatMode = false, runtimeGoalState = null) {
4605
+ const newObjective = codexGoalObjective(newMission);
4606
+ const pausedConflict = runtimeGoalState?.status === 'paused';
4607
+ const holdOldMission = `atris mission pause ${activeMission.id} --reason ${shellQuote(`visible goal replaced by ${newMission.id}`)} --json`;
4608
+ const ackNewMission = codexGoalAckCommand(newMission, newObjective);
4609
+ const completeOldMission = `atris mission complete ${activeMission.id} --proof "<proof>" --json`;
4610
+ const refreshAfterResume = `atris mission goal --native-goal-status active --native-goal-objective ${shellQuote(codexGoalObjective(activeMission))} --json`;
4611
+ const nextCommand = pausedConflict
4612
+ ? `Resume the paused Codex goal, then run ${refreshAfterResume}`
4613
+ : `${holdOldMission} && ${ackNewMission}`;
4614
+ const commands = {
4615
+ hold_old_mission: holdOldMission,
4616
+ complete_old_mission: completeOldMission,
4617
+ refresh_after_resume: refreshAfterResume,
4618
+ ack_new_mission: ackNewMission,
4619
+ };
4620
+ const nativeGoalAction = codexNativeGoalReplaceAction(newMission, activeMission, runtimeGoalState, commands);
4621
+ const nativeGoalResolution = {
4622
+ action: 'replace_visible_goal',
4623
+ automatic_decision: true,
4624
+ executable_now: false,
4625
+ blocked_by: nativeGoalAction.blocked_by,
4626
+ required_tool: nativeGoalAction.tool,
4627
+ from_mission_id: activeMission.id,
4628
+ to_mission_id: newMission.id,
4629
+ };
4630
+ const conflict = {
4631
+ schema: 'atris.codex_active_goal_conflict.v1',
4632
+ message: pausedConflict
4633
+ ? `new mission created, but old mission ${activeMission.id} is paused and still occupies the visible slot.`
4634
+ : `new mission created, but old mission ${activeMission.id} still owns the visible slot.`,
4635
+ source: request?.source || 'mission_run_objective',
4636
+ status: pausedConflict ? 'paused_goal_conflict' : 'active_goal_conflict',
4637
+ new_mission_id: newMission.id,
4638
+ new_objective: newObjective,
4639
+ active_mission_id: activeMission.id,
4640
+ active_objective: codexGoalObjective(activeMission),
4641
+ next_command: nextCommand,
4642
+ native_goal_action: nativeGoalAction,
4643
+ native_goal_resolution: nativeGoalResolution,
4644
+ commands,
4645
+ new_mission: missionStatusView(newMission),
4646
+ active_mission: missionStatusView(activeMission),
4647
+ request,
4648
+ runtime_goal_state: runtimeGoalState,
4649
+ };
4650
+ return {
4651
+ ok: true,
4652
+ action: pausedConflict ? 'paused_goal_conflict' : 'active_goal_conflict',
4653
+ active_goal_conflict: conflict,
4654
+ mission: conflict.new_mission,
4655
+ conflicting_mission: conflict.active_mission,
4656
+ goal: null,
4657
+ heartbeat: heartbeatMode ? { heavy_work_performed: false, next_heavy_command: nextCommand } : undefined,
4658
+ requires_native_goal_start: true,
4659
+ requires_native_goal_replace: true,
4660
+ native_goal_action: nativeGoalAction,
4661
+ native_goal_resolution: nativeGoalResolution,
4662
+ next_command: nextCommand,
4663
+ };
1827
4664
  }
1828
4665
 
1829
- function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
1830
- const candidates = listMissions(root)
1831
- .filter((mission) => missionSelectableForCodexGoal(mission, now));
4666
+ function codexNativeGoalAction(mission, objective = codexGoalObjective(mission)) {
4667
+ return {
4668
+ runtime: 'codex',
4669
+ tool: 'create_goal',
4670
+ args: { objective },
4671
+ };
4672
+ }
1832
4673
 
1833
- candidates.sort((a, b) => {
1834
- const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
1835
- const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
1836
- if (aCaller !== bCaller) return bCaller - aCaller;
4674
+ function codexNativeGoalStartInstruction(mission, objective = codexGoalObjective(mission)) {
4675
+ return `Call native Codex create_goal({ objective: ${JSON.stringify(objective)} }), then run ${codexGoalAckCommand(mission, objective)}`;
4676
+ }
1837
4677
 
1838
- const aVerifier = a.verifier ? 1 : 0;
1839
- const bVerifier = b.verifier ? 1 : 0;
1840
- if (aVerifier !== bVerifier) return bVerifier - aVerifier;
4678
+ function codexNativeGoalReplaceInstruction(mission, runtimeGoalState = null, objective = codexGoalObjective(mission), options = {}) {
4679
+ const fromObjective = runtimeGoalState?.objective
4680
+ ? ` from paused objective ${JSON.stringify(runtimeGoalState.objective)}`
4681
+ : '';
4682
+ if (options.allowNativeGoalSupersede === true) {
4683
+ return `Supersede approved: run update_goal({ status: "complete" }), then create_goal({ objective: ${JSON.stringify(objective)} }), then run ${codexGoalAckCommand(mission, objective)}. Atris records the old paused goal as superseded.`;
4684
+ }
4685
+ return `Native Codex replace_goal is required${fromObjective} to ${JSON.stringify(objective)}, then run ${codexGoalAckCommand(mission, objective)}; this runtime currently lacks replace_goal. Fallback is update_goal({ status: "complete" }) -> create_goal({ objective: ${JSON.stringify(objective)} }) -> mission goal ack only after handoff proof says the paused goal is intentionally superseded.`;
4686
+ }
1841
4687
 
1842
- const aTime = missionSortTime(a);
1843
- const bTime = missionSortTime(b);
1844
- return bTime - aTime;
1845
- });
4688
+ function codexNativeGoalBlockPayload(mission) {
4689
+ const objective = codexGoalObjective(mission);
4690
+ return {
4691
+ ok: false,
4692
+ code: 'native_goal_not_started',
4693
+ mission_id: mission.id,
4694
+ objective,
4695
+ requires_native_goal_start: true,
4696
+ native_goal_action: codexNativeGoalAction(mission, objective),
4697
+ native_goal_ack_command: codexGoalAckCommand(mission, objective),
4698
+ next_action: codexNativeGoalStartInstruction(mission, objective),
4699
+ };
4700
+ }
1846
4701
 
1847
- const mission = candidates[0] || null;
1848
- if (!mission) return null;
1849
- const due = mission.verifier && missionDueAt(mission, now);
1850
- return { mission, reason: due ? 'due' : 'active' };
4702
+ function maybeBlockUntilCodexNativeGoalStarted(mission, asJson) {
4703
+ if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return;
4704
+ const payload = codexNativeGoalBlockPayload(mission);
4705
+ if (asJson) console.log(JSON.stringify(payload, null, 2));
4706
+ else console.error(payload.next_action);
4707
+ process.exit(2);
1851
4708
  }
1852
4709
 
1853
- function codexGoalObjective(mission) {
1854
- return mission.objective;
4710
+ function returnIfCodexNativeGoalNotStarted(mission, asJson) {
4711
+ if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return false;
4712
+ const payload = codexNativeGoalBlockPayload(mission);
4713
+ if (asJson) console.log(JSON.stringify(payload, null, 2));
4714
+ else console.error(payload.next_action);
4715
+ process.exitCode = 2;
4716
+ return true;
1855
4717
  }
1856
4718
 
1857
4719
  function codexGoalNextCommand(mission) {
4720
+ if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
4721
+ return codexNativeGoalStartInstruction(mission);
4722
+ }
1858
4723
  const taskSpine = missionTaskSpine(mission);
1859
4724
  if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) {
1860
4725
  return taskSpine.ensure_task_command;
1861
4726
  }
4727
+ if (missionChoosesNextMission(mission)) {
4728
+ return chooseNextMissionCommand(mission);
4729
+ }
4730
+ if (mission.status === 'ready' && mission.always_on) {
4731
+ return nextCandidateRunCommand(mission);
4732
+ }
1862
4733
  if (mission.status === 'ready') {
1863
4734
  const xpAction = missionXpReadyAction(mission, mission.receipt_path);
1864
4735
  if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
1865
4736
  }
1866
- if (mission.verifier && missionDueAt(mission)) {
4737
+ const verifier = effectiveMissionVerifier(mission);
4738
+ if (verifier && missionDueAt(mission)) {
1867
4739
  const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
1868
4740
  return `atris mission run --due --max-ticks 1${completeFlag}`;
1869
4741
  }
1870
- if (mission.verifier) {
4742
+ if (verifier) {
1871
4743
  return `atris mission tick ${mission.id} --verify --summary "<what changed>"`;
1872
4744
  }
1873
4745
  return `atris mission tick ${mission.id} --summary "<what changed>"`;
1874
4746
  }
1875
4747
 
1876
4748
  function codexVisibleGoalBridge(mission, goalObjective) {
4749
+ const ack = codexNativeGoalAck(mission, goalObjective);
1877
4750
  return {
1878
4751
  schema: 'atris.visible_chat_goal_bridge.v1',
1879
4752
  runtime: 'codex',
1880
4753
  source: 'atris_mission',
1881
4754
  mission_id: mission.id,
1882
4755
  desired_objective: goalObjective,
1883
- status: 'needs_runtime_write',
4756
+ status: ack ? 'active' : 'needs_runtime_write',
4757
+ acknowledged_at: ack?.acknowledged_at || null,
1884
4758
  state_file: '.atris/state/codex_goal.json',
1885
4759
  status_file: 'atris/status/codex-goal.md',
1886
4760
  operations: {
1887
4761
  read_current_goal: 'get_goal',
1888
4762
  keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
1889
4763
  create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
4764
+ ack_after_create: codexGoalAckCommand(mission, goalObjective),
1890
4765
  complete_after_proof: 'update_goal({ status: "complete" })',
1891
4766
  refresh_on_phase_change: 'atris mission goal --json before continuing changed work',
1892
4767
  refresh_next_candidate: 'atris mission goal --json',
@@ -1894,6 +4769,7 @@ function codexVisibleGoalBridge(mission, goalObjective) {
1894
4769
  guardrails: [
1895
4770
  'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
1896
4771
  'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
4772
+ 'Do not run mission work for runner=codex_goal until ack_after_create has been recorded.',
1897
4773
  'When the work changes phase or objective, refresh the mission goal before continuing.',
1898
4774
  ],
1899
4775
  };
@@ -1909,7 +4785,7 @@ function codexGoalToolContract(mission) {
1909
4785
  visible_goal_bridge: 'goal.visible_goal',
1910
4786
  platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
1911
4787
  phase_change_refresh: 'before changed follow-up work, run atris mission goal --json and mirror the returned visible goal',
1912
- runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof or phase change -> atris mission goal --json -> create_goal({ objective: goal.objective })',
4788
+ runtime_tool_sequence: 'get_goal -> create_goal({ objective }) -> atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json -> do work -> update_goal({ status: "complete" }) after proof or phase change -> atris mission goal --json',
1913
4789
  blocked_without_platform_goal_write: true,
1914
4790
  mission_id: mission.id,
1915
4791
  };
@@ -1957,6 +4833,15 @@ function writeCodexGoalState(payload, root = process.cwd()) {
1957
4833
  lines.push(`- visible goal complete: ${state.goal.visible_goal.operations.complete_after_proof}`);
1958
4834
  }
1959
4835
  lines.push(`- platform write blocked: ${state.goal.codex_tool_contract.blocked_without_platform_goal_write}`);
4836
+ } else if (state.active_goal_conflict) {
4837
+ lines.push(`- conflict: ${state.active_goal_conflict.message}`);
4838
+ lines.push(`- new mission: ${state.active_goal_conflict.new_mission_id}`);
4839
+ lines.push(`- active mission: ${state.active_goal_conflict.active_mission_id}`);
4840
+ if (state.active_goal_conflict.native_goal_action) {
4841
+ lines.push(`- native goal action: ${state.active_goal_conflict.native_goal_action.tool}`);
4842
+ lines.push(`- native goal executable now: ${state.active_goal_conflict.native_goal_action.available !== false}`);
4843
+ }
4844
+ lines.push(`- next: ${state.active_goal_conflict.next_command}`);
1960
4845
  } else {
1961
4846
  lines.push('- mission: none');
1962
4847
  }
@@ -1972,7 +4857,34 @@ function writeCodexGoalState(payload, root = process.cwd()) {
1972
4857
 
1973
4858
  function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1974
4859
  const heartbeatMode = options.heartbeat === true;
1975
- const selected = selectCodexGoalMission(root);
4860
+ const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
4861
+ const directRequest = options.missionId
4862
+ ? (() => {
4863
+ const mission = resolveMission(options.missionId, root);
4864
+ return mission && isCodexGoalMission(mission) && missionSelectableForCodexGoal(mission)
4865
+ ? { mission, request: null }
4866
+ : null;
4867
+ })()
4868
+ : readDirectRunCodexGoalRequest(root);
4869
+ const activeGoalOwner = directRequest
4870
+ ? activeCodexVisibleGoalOwner(root, directRequest.mission.id, new Date(), runtimeGoalState)
4871
+ : null;
4872
+ if (directRequest && activeGoalOwner) {
4873
+ return codexGoalActiveConflictPayload(directRequest.mission, activeGoalOwner, directRequest.request, heartbeatMode, runtimeGoalState);
4874
+ }
4875
+ let selected = directRequest
4876
+ ? { mission: directRequest.mission, reason: 'direct_run', direct_goal_request: directRequest.request }
4877
+ : selectCodexGoalMission(root, options);
4878
+ if (!selected && !directRequest) {
4879
+ const continuation = seedNextMoveContinuationGoal(root);
4880
+ if (continuation?.mission) {
4881
+ selected = {
4882
+ mission: continuation.mission,
4883
+ reason: continuation.inserted ? 'next_move_continuation_seeded' : 'next_move_continuation_active',
4884
+ seeded_continuation_goal: continuation,
4885
+ };
4886
+ }
4887
+ }
1976
4888
  if (!selected) {
1977
4889
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(null, null) : undefined;
1978
4890
  return {
@@ -1983,10 +4895,20 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1983
4895
  };
1984
4896
  }
1985
4897
 
1986
- const { mission, reason } = selected;
4898
+ const { mission, reason, direct_goal_request: directGoalRequest, seeded_continuation_goal: seededContinuationGoal } = selected;
1987
4899
  const taskSpine = missionTaskSpine(mission);
1988
4900
  const missionView = missionStatusView(mission);
1989
4901
  const objective = codexGoalObjective(mission);
4902
+ const ack = codexNativeGoalAck(mission, objective);
4903
+ const runtimeNeedsReplace = !ack && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
4904
+ const nativeGoalAction = ack
4905
+ ? null
4906
+ : runtimeNeedsReplace
4907
+ ? codexNativeGoalRuntimeReplaceAction(mission, runtimeGoalState, {
4908
+ ack_new_mission: codexGoalAckCommand(mission, objective),
4909
+ allow_native_goal_supersede: options.allowNativeGoalSupersede === true,
4910
+ })
4911
+ : codexNativeGoalAction(mission, objective);
1990
4912
  const goal = {
1991
4913
  objective,
1992
4914
  mission_id: mission.id,
@@ -1996,10 +4918,23 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1996
4918
  executed_by: taskSpine?.executed_by || mission.executed_by || null,
1997
4919
  task_spine: taskSpine,
1998
4920
  reason,
1999
- next_command: codexGoalNextCommand(mission),
4921
+ next_command: runtimeNeedsReplace
4922
+ ? codexNativeGoalReplaceInstruction(mission, runtimeGoalState, objective, {
4923
+ allowNativeGoalSupersede: options.allowNativeGoalSupersede === true,
4924
+ })
4925
+ : codexGoalNextCommand(mission),
2000
4926
  replace_after: 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.',
2001
4927
  visible_goal: codexVisibleGoalBridge(mission, objective),
2002
4928
  codex_tool_contract: codexGoalToolContract(mission),
4929
+ requires_native_goal_start: !ack,
4930
+ requires_native_goal_replace: runtimeNeedsReplace,
4931
+ native_goal_action: nativeGoalAction,
4932
+ native_goal_ack_command: codexGoalAckCommand(mission, objective),
4933
+ native_goal_ack: ack,
4934
+ direct_goal_request: directGoalRequest || null,
4935
+ seeded_continuation_goal: seededContinuationGoal || null,
4936
+ next_action_preview: missionChoosesNextMission(mission) ? chooseNextMissionPreview(mission, root) : (mission.next_action_preview || null),
4937
+ runtime_goal_state: runtimeGoalState,
2003
4938
  };
2004
4939
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
2005
4940
  return {
@@ -2008,6 +4943,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
2008
4943
  goal,
2009
4944
  mission: missionView,
2010
4945
  heartbeat,
4946
+ requires_native_goal_start: goal.requires_native_goal_start,
4947
+ requires_native_goal_replace: goal.requires_native_goal_replace,
4948
+ native_goal_action: goal.native_goal_action,
4949
+ runtime_goal_state: runtimeGoalState,
2011
4950
  };
2012
4951
  }
2013
4952
 
@@ -2021,6 +4960,138 @@ function refreshCodexGoalController(root = process.cwd(), options = {}) {
2021
4960
  };
2022
4961
  }
2023
4962
 
4963
+ function atrisVisibleGoalBridge(mission, goalObjective) {
4964
+ return {
4965
+ schema: 'atris.visible_goal_bridge.v1',
4966
+ runtime: String(mission.runner || 'manual'),
4967
+ source: 'atris_mission',
4968
+ mission_id: mission.id,
4969
+ desired_objective: goalObjective,
4970
+ status: 'active',
4971
+ state_file: '.atris/state/atris_goal.json',
4972
+ status_file: 'atris/status/atris-goal.md',
4973
+ operations: {
4974
+ read_current_goal: 'atris mission goal --runtime atris --json',
4975
+ update_from_mission: 'atris mission tick <mission-id> --summary "<what changed>" --json',
4976
+ refresh_on_phase_change: 'atris mission goal --runtime atris --json before continuing changed work',
4977
+ complete_after_proof: 'atris mission complete <mission-id> --proof "<receipt_path>" --json',
4978
+ },
4979
+ guardrails: [
4980
+ 'Atris owns this goal state; no external native-goal tool ack is required.',
4981
+ 'Do not claim completion until the mission has proof or a recorded blocked human ask.',
4982
+ 'Use the mission task spine when task_spine.current_step_command is present.',
4983
+ ],
4984
+ };
4985
+ }
4986
+
4987
+ function atrisGoalToolContract(mission) {
4988
+ return {
4989
+ current_policy: 'keep one Atris-owned visible goal active for mission/chat/fast runtimes',
4990
+ read_current_goal: 'atris mission goal --runtime atris --json',
4991
+ set_next_goal: 'atris mission run "<objective>" --runner atris2|manual|claude',
4992
+ complete_current_goal: 'atris mission complete <mission-id> --proof "<receipt_path>" --json',
4993
+ visible_goal_bridge: 'goal.visible_goal',
4994
+ platform_requirement: 'No native platform goal tool is required; Atris mission/task state is the source of truth.',
4995
+ runtime_tool_sequence: 'atris mission run -> atris_goal_state active -> ax fast / atris chat shows goal -> mission tick/task proof -> mission complete',
4996
+ blocked_without_platform_goal_write: false,
4997
+ mission_id: mission.id,
4998
+ };
4999
+ }
5000
+
5001
+ function buildAtrisGoalPayload(root = process.cwd(), options = {}) {
5002
+ const heartbeatMode = options.heartbeat === true;
5003
+ const selected = selectAtrisGoalMission(root, options);
5004
+ if (!selected) {
5005
+ return {
5006
+ ok: true,
5007
+ action: heartbeatMode ? 'atris_goal_heartbeat' : 'no_goal_candidate',
5008
+ mission: null,
5009
+ heartbeat: heartbeatMode ? { heavy_work_performed: false, next_heavy_command: null } : undefined,
5010
+ };
5011
+ }
5012
+
5013
+ const { mission, reason } = selected;
5014
+ const taskSpine = missionTaskSpine(mission);
5015
+ const missionView = missionStatusView(mission);
5016
+ const objective = mission.objective;
5017
+ const goal = {
5018
+ objective,
5019
+ mission_id: mission.id,
5020
+ mission_objective: mission.objective,
5021
+ mission_status: mission.status,
5022
+ owner: taskSpine?.owner || mission.owner,
5023
+ executed_by: taskSpine?.executed_by || mission.executed_by || null,
5024
+ runner: mission.runner || 'manual',
5025
+ model: mission.model || null,
5026
+ task_spine: taskSpine,
5027
+ reason,
5028
+ next_command: codexGoalNextCommand(mission),
5029
+ replace_after: 'After proof or phase change, run atris mission goal --runtime atris --json and continue from the returned next_command.',
5030
+ visible_goal: atrisVisibleGoalBridge(mission, objective),
5031
+ atris_tool_contract: atrisGoalToolContract(mission),
5032
+ requires_native_goal_start: false,
5033
+ native_goal_action: null,
5034
+ };
5035
+ return {
5036
+ ok: true,
5037
+ action: heartbeatMode ? 'atris_goal_heartbeat' : 'atris_goal_candidate',
5038
+ goal,
5039
+ mission: missionView,
5040
+ heartbeat: heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined,
5041
+ requires_native_goal_start: false,
5042
+ native_goal_action: null,
5043
+ };
5044
+ }
5045
+
5046
+ function writeAtrisGoalState(payload, root = process.cwd()) {
5047
+ const paths = statePaths(root);
5048
+ const state = {
5049
+ schema: 'atris.goal_controller.v1',
5050
+ updated_at: stampIso(),
5051
+ ...payload,
5052
+ };
5053
+ fs.mkdirSync(path.dirname(paths.atrisGoalJson), { recursive: true });
5054
+ fs.writeFileSync(paths.atrisGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
5055
+
5056
+ const lines = [
5057
+ '# Atris Goal Controller',
5058
+ '',
5059
+ '<!-- Generated by Atris. Do not hand-edit. -->',
5060
+ '',
5061
+ `- updated: ${state.updated_at}`,
5062
+ `- action: ${state.action}`,
5063
+ ];
5064
+ if (state.goal) {
5065
+ lines.push(`- mission: ${state.goal.mission_id}`);
5066
+ lines.push(`- runner: ${state.goal.runner}`);
5067
+ lines.push(`- status: ${state.goal.mission_status}`);
5068
+ lines.push(`- reason: ${state.goal.reason}`);
5069
+ lines.push(`- objective: ${state.goal.objective}`);
5070
+ lines.push(`- next: ${state.goal.next_command}`);
5071
+ lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
5072
+ } else {
5073
+ lines.push('- mission: none');
5074
+ }
5075
+ lines.push('');
5076
+ fs.mkdirSync(path.dirname(paths.atrisGoalStatus), { recursive: true });
5077
+ fs.writeFileSync(paths.atrisGoalStatus, lines.join('\n'), 'utf8');
5078
+ return {
5079
+ state_path: paths.atrisGoalJson,
5080
+ status_path: paths.atrisGoalStatus,
5081
+ state,
5082
+ };
5083
+ }
5084
+
5085
+ function refreshAtrisGoalController(root = process.cwd(), options = {}) {
5086
+ const payload = buildAtrisGoalPayload(root, options);
5087
+ const rendered = writeAtrisGoalState(payload, root);
5088
+ return {
5089
+ ...payload,
5090
+ state_path: rendered.state_path,
5091
+ status_path: rendered.status_path,
5092
+ };
5093
+ }
5094
+
2024
5095
  function runAtrisMissionJsonCommand(root, args, options = {}) {
2025
5096
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
2026
5097
  const result = spawnSync(process.execPath, [cliPath, ...args], {
@@ -2089,6 +5160,7 @@ function goalLoopNextCommandPlan(goal) {
2089
5160
 
2090
5161
  function shouldRunGoalLoopCommand(heartbeat, plan) {
2091
5162
  if (!heartbeat?.goal) return false;
5163
+ if (heartbeat.goal.requires_native_goal_start === true) return false;
2092
5164
  if (plan && plan.run_when_due_only === false) return true;
2093
5165
  return heartbeat.heartbeat?.due === true;
2094
5166
  }
@@ -2138,6 +5210,19 @@ function computeBackoff(policy, attempt) {
2138
5210
  return Math.min(policy.maxMs, Math.round(base + jitter));
2139
5211
  }
2140
5212
 
5213
+ function missionBudgetPromptLines(mission) {
5214
+ const contract = mission?.budget_contract;
5215
+ if (!contract) return [];
5216
+ return [
5217
+ ``,
5218
+ `## Budget`,
5219
+ `Plain rule: ${contract.plain_language}`,
5220
+ `Limit: ${contract.budget_label}`,
5221
+ `Stop rule: ${contract.stop_rule}`,
5222
+ `Before acting, state the current bottleneck, the next useful move, and what proof or stop reason will make the tick honest.`,
5223
+ ];
5224
+ }
5225
+
2141
5226
  function consecutiveVerifierFails(ticks) {
2142
5227
  let n = 0;
2143
5228
  for (let i = ticks.length - 1; i >= 0; i--) {
@@ -2180,22 +5265,36 @@ function isWithinActiveHours(activeHours, now = new Date()) {
2180
5265
  return cur >= start || cur < end;
2181
5266
  }
2182
5267
 
2183
- function acquireMissionLock(missionId, root = process.cwd()) {
5268
+ function sleepSync(ms) {
5269
+ const waitMs = Math.max(0, Number(ms) || 0);
5270
+ if (!waitMs) return;
5271
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
5272
+ }
5273
+
5274
+ function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
2184
5275
  const dir = path.join(root, '.atris', 'state');
2185
5276
  fs.mkdirSync(dir, { recursive: true });
2186
5277
  const lockFile = path.join(dir, `mission-${missionId}.lock`);
5278
+ const waitMs = Math.max(0, Number(options.waitMs) || 0);
5279
+ const deadline = waitMs ? Date.now() + waitMs : 0;
2187
5280
  let fd;
2188
- try {
2189
- fd = fs.openSync(lockFile, 'wx');
2190
- fs.writeSync(fd, JSON.stringify({ pid: process.pid, started_at: stampIso(), mission_id: missionId }));
2191
- return { ok: true, lockFile, fd };
2192
- } catch (e) {
2193
- if (e.code === 'EEXIST') {
2194
- let info = {};
2195
- try { info = JSON.parse(fs.readFileSync(lockFile, 'utf8') || '{}'); } catch {}
2196
- return { ok: false, lockFile, busy: true, holder: info };
5281
+ while (true) {
5282
+ try {
5283
+ fd = fs.openSync(lockFile, 'wx');
5284
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, started_at: stampIso(), mission_id: missionId }));
5285
+ return { ok: true, lockFile, fd };
5286
+ } catch (e) {
5287
+ if (e.code === 'EEXIST') {
5288
+ let info = {};
5289
+ try { info = JSON.parse(fs.readFileSync(lockFile, 'utf8') || '{}'); } catch {}
5290
+ if (deadline && Date.now() < deadline) {
5291
+ sleepSync(Math.min(25, deadline - Date.now()));
5292
+ continue;
5293
+ }
5294
+ return { ok: false, lockFile, busy: true, holder: info };
5295
+ }
5296
+ return { ok: false, lockFile, error: e.message };
2197
5297
  }
2198
- return { ok: false, lockFile, error: e.message };
2199
5298
  }
2200
5299
  }
2201
5300
 
@@ -2228,6 +5327,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen) {
2228
5327
  `**Verifier (frozen):** ${frozen.verifier || '(none — receipt only)'}`,
2229
5328
  `**Last status:** ${mission.status}`,
2230
5329
  `**Last tick:** ${mission.last_tick_at || 'never'}`,
5330
+ ...missionBudgetPromptLines(mission),
2231
5331
  ``,
2232
5332
  `## Your task`,
2233
5333
  `Do ONE increment of work toward the stop condition. ONE. No more.`,
@@ -2434,9 +5534,11 @@ async function runMission(args) {
2434
5534
  const verifyEach = !hasFlag(args, '--no-verify');
2435
5535
  const completeOnPass = hasFlag(args, '--complete-on-pass');
2436
5536
  const skipDrain = hasFlag(args, '--no-drain');
5537
+ const createNext = hasFlag(args, '--create-next');
2437
5538
  const maxTicksFlag = readFlag(args, '--max-ticks', '');
2438
5539
  const maxTicks = Math.max(1, Number(maxTicksFlag) || MISSION_RUN_DEFAULTS.maxTicks);
2439
- const maxWallSeconds = Math.max(60, Number(readFlag(args, '--max-wall', '')) || MISSION_RUN_DEFAULTS.maxWallSeconds);
5540
+ const maxWallFlag = readFlag(args, '--max-wall', '');
5541
+ let maxWallSeconds = Math.max(60, Number(maxWallFlag) || MISSION_RUN_DEFAULTS.maxWallSeconds);
2440
5542
  const cadenceOverride = readFlag(args, '--cadence', '');
2441
5543
  const input = missionRunInputFromArgs(args);
2442
5544
  const ref = input.ref;
@@ -2467,6 +5569,9 @@ async function runMission(args) {
2467
5569
  if (!mission) {
2468
5570
  exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
2469
5571
  }
5572
+ if (!maxWallFlag && Number(mission.budget_contract?.requested_seconds) > 0) {
5573
+ maxWallSeconds = Math.max(60, Number(mission.budget_contract.requested_seconds));
5574
+ }
2470
5575
  if (['complete', 'stopped'].includes(mission.status)) {
2471
5576
  if (asJson) {
2472
5577
  printJsonOrText(
@@ -2480,6 +5585,8 @@ async function runMission(args) {
2480
5585
  process.exit(0);
2481
5586
  }
2482
5587
 
5588
+ maybeBlockUntilCodexNativeGoalStarted(mission, asJson);
5589
+
2483
5590
  const preLockRunner = String(mission.runner || '').trim().toLowerCase();
2484
5591
  const preLockCallerSession = runnerUsesCallerSession(mission.runner);
2485
5592
  if (!skipClaude && !preLockCallerSession && preLockRunner !== 'atris2') {
@@ -2520,10 +5627,12 @@ async function runMission(args) {
2520
5627
  console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
2521
5628
  return;
2522
5629
  }
5630
+ if (returnIfCodexNativeGoalNotStarted(mission, asJson)) return;
2523
5631
  if (mission.status === 'paused') {
2524
5632
  mission = saveMission({
2525
5633
  ...mission,
2526
5634
  status: 'running',
5635
+ paused_at: null,
2527
5636
  resumed_at: stampIso(),
2528
5637
  stop_reason: null,
2529
5638
  next_action: `running: atris mission run ${mission.id}`,
@@ -2537,7 +5646,7 @@ async function runMission(args) {
2537
5646
 
2538
5647
  // Freeze run-start contract (verifier, lane). Stored on receipts, not the mission record.
2539
5648
  const frozen = {
2540
- verifier: mission.verifier || '',
5649
+ verifier: effectiveMissionVerifier(mission),
2541
5650
  lane: mission.lane || 'workspace',
2542
5651
  started_at: stampIso(),
2543
5652
  };
@@ -2565,7 +5674,9 @@ async function runMission(args) {
2565
5674
  : atris2Runner
2566
5675
  ? `atris2 (${mission.model || 'atris:fast'})`
2567
5676
  : (sessionId || `pending=${pendingSessionId}`);
2568
- console.error(`[mission run] ${mission.id}\n objective: ${mission.objective}\n lane: ${frozen.lane}\n cadence: ${cadence} (${cadenceSeconds}s)\n max_ticks: ${effectiveMaxTicks}, max_wall: ${maxWallSeconds}s\n session: ${sessionLabel}`);
5677
+ if (!asJson) {
5678
+ console.error(`[mission run] ${mission.id}\n objective: ${mission.objective}\n lane: ${frozen.lane}\n cadence: ${cadence} (${cadenceSeconds}s)\n max_ticks: ${effectiveMaxTicks}, max_wall: ${maxWallSeconds}s\n session: ${sessionLabel}`);
5679
+ }
2569
5680
 
2570
5681
  while (ticks.length < effectiveMaxTicks) {
2571
5682
  const elapsedSec = (Date.now() - startedAt) / 1000;
@@ -2576,7 +5687,7 @@ async function runMission(args) {
2576
5687
  // Re-read mission, detect mutation of frozen fields
2577
5688
  mission = resolveMission(mission.id) || mission;
2578
5689
  if (['complete', 'stopped', 'paused'].includes(mission.status)) { pauseReason = mission.status; break; }
2579
- if (mission.verifier !== frozen.verifier) { pauseReason = 'verifier-mutated'; break; }
5690
+ if (effectiveMissionVerifier(mission) !== frozen.verifier) { pauseReason = 'verifier-mutated'; break; }
2580
5691
  if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
2581
5692
 
2582
5693
  const tickIdx = Number(mission.last_tick_index || 0) + 1;
@@ -2729,9 +5840,12 @@ async function runMission(args) {
2729
5840
  });
2730
5841
 
2731
5842
  const xpReadyAction = missionXpReadyAction(mission, receiptPath);
5843
+ const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
5844
+ const fullBudgetMode = budgetRemainingSeconds > 0;
2732
5845
  const newStatus = (verifierResult?.passed && mission.always_on) ? 'ready' :
2733
5846
  (verifierResult?.passed && xpReadyAction) ? 'ready' :
2734
- (verifierResult?.passed && completeOnPass) ? 'complete' :
5847
+ (verifierResult?.passed && completeOnPass && !fullBudgetMode) ? 'complete' :
5848
+ (verifierResult?.passed && fullBudgetMode) ? 'running' :
2735
5849
  (verifierResult?.passed ? 'ready' :
2736
5850
  (verifierResult ? 'blocked' :
2737
5851
  (result.status === 'ran' ? 'running' : mission.status)));
@@ -2740,16 +5854,22 @@ async function runMission(args) {
2740
5854
  nextAction = nextCandidateTickAction(mission);
2741
5855
  } else if (verifierResult?.passed && xpReadyAction) {
2742
5856
  nextAction = xpReadyAction;
2743
- } else if (verifierResult?.passed && completeOnPass) {
5857
+ } else if (verifierResult?.passed && completeOnPass && !fullBudgetMode) {
2744
5858
  nextAction = 'mission complete';
5859
+ } else if (verifierResult?.passed && fullBudgetMode) {
5860
+ nextAction = `proof passed with ${durationLabel(budgetRemainingSeconds)} left on the budget; pick the next useful move, then: atris mission tick ${mission.id} --verify --summary "<what changed>"`;
2745
5861
  } else if (verifierResult?.passed) {
2746
5862
  nextAction = `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`;
2747
5863
  } else if (verifierResult) {
2748
5864
  nextAction = 'fix verifier failure or revise mission';
5865
+ } else if (result.status === 'ran' && mission.always_on) {
5866
+ nextAction = nextCandidateTickAction(mission);
2749
5867
  }
2750
5868
  mission = saveMission({
2751
5869
  ...mission,
2752
5870
  status: newStatus,
5871
+ paused_at: null,
5872
+ stop_reason: null,
2753
5873
  last_tick_at: finishedAt,
2754
5874
  last_tick_status: result.status,
2755
5875
  last_tick_reason: result.reason,
@@ -2777,7 +5897,9 @@ async function runMission(args) {
2777
5897
  }
2778
5898
  refreshCodexGoalController(cwd);
2779
5899
 
2780
- console.error(`[tick ${tickIdx}] status=${result.status} reason=${result.reason} verifier=${verifierResult ? (verifierResult.passed ? 'pass' : 'fail') : 'skip'} -> ${receiptPath || '-'}`);
5900
+ if (!asJson) {
5901
+ console.error(`[tick ${tickIdx}] status=${result.status} reason=${result.reason} verifier=${verifierResult ? (verifierResult.passed ? 'pass' : 'fail') : 'skip'} -> ${receiptPath || '-'}`);
5902
+ }
2781
5903
 
2782
5904
  if (result.status === 'ran') {
2783
5905
  ranTicks++;
@@ -2786,7 +5908,8 @@ async function runMission(args) {
2786
5908
  backoffAttempt++;
2787
5909
  }
2788
5910
 
2789
- if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on)) break;
5911
+ if (callerSessionRunner && result.status === 'ran') break;
5912
+ if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on && !fullBudgetMode)) break;
2790
5913
  if (consecutiveVerifierFails(ticks) >= 2) { pauseReason = 'consecutive-verifier-fails'; break; }
2791
5914
  // A retired/inaccessible model is deterministic: the id is fixed for the run, so
2792
5915
  // every remaining tick (and every future cron firing) fails identically. Backoff
@@ -2837,6 +5960,17 @@ async function runMission(args) {
2837
5960
  }
2838
5961
 
2839
5962
  const summaryWorktree = worktreeReceipt(runWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
5963
+ const createdNext = createNext
5964
+ ? require('./loop-front').createNextLoopTask(['--as', mission.owner || 'auto-improver', '--json'], cwd, { print: false })
5965
+ : null;
5966
+ const landingSummary = {
5967
+ changed: missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks, createdNext),
5968
+ timeline_command: missionRunTimelineCommand(mission),
5969
+ export_command: missionRunExportCommand(mission),
5970
+ prune_preview_command: missionRunPrunePreviewCommand(mission),
5971
+ next: missionRunCreatedNextLine(createdNext, continuationGoal, mission),
5972
+ };
5973
+ landingSummary.reason = missionHumanReasonText(mission, landingSummary.changed);
2840
5974
  const finalReceipt = writeReceipt(mission, {
2841
5975
  kind: 'mission_run_summary',
2842
5976
  frozen,
@@ -2847,22 +5981,17 @@ async function runMission(args) {
2847
5981
  session_id: sessionId,
2848
5982
  pending_session_id: mission.pending_session_id || null,
2849
5983
  elapsed_seconds: (Date.now() - startedAt) / 1000,
5984
+ budget_contract: mission.budget_contract || null,
2850
5985
  worktree: summaryWorktree,
5986
+ created_next: createdNext,
5987
+ landing: landingSummary,
2851
5988
  });
5989
+ const atrisGoalState = refreshAtrisGoalController(cwd, { missionId: mission.id });
2852
5990
  const codexGoalState = refreshCodexGoalController(cwd);
2853
5991
 
2854
5992
  printJsonOrText(
2855
- { ok: true, action: 'mission_run', mission, ran_ticks: ranTicks, tick_count: ticks.length, ticks, pause_reason: pauseReason, session_id: sessionId, summary_receipt: finalReceipt, worktree: summaryWorktree, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
2856
- [
2857
- `Ran mission ${mission.id}`,
2858
- ` objective: ${mission.objective}`,
2859
- ` ran_ticks: ${ranTicks}/${effectiveMaxTicks} (skipped/errored: ${ticks.length - ranTicks})`,
2860
- ` final state: ${mission.status}`,
2861
- pauseReason ? ` pause: ${pauseReason}` : null,
2862
- ` session: ${sessionId || '(none)'}`,
2863
- ` summary receipt: ${finalReceipt}`,
2864
- continuationGoal?.mission ? ` next goal: ${continuationGoal.mission.objective}` : null,
2865
- ].filter(Boolean),
5993
+ { ok: true, action: 'mission_run', mission, ran_ticks: ranTicks, tick_count: ticks.length, ticks, pause_reason: pauseReason, session_id: sessionId, summary_receipt: finalReceipt, budget_contract: mission.budget_contract || null, worktree: summaryWorktree, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal, created_next: createdNext },
5994
+ missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalReceipt, pauseReason, continuationGoal, ticks, createdNext),
2866
5995
  asJson,
2867
5996
  );
2868
5997
  } finally {
@@ -2877,9 +6006,10 @@ async function runMission(args) {
2877
6006
  function tickMission(args) {
2878
6007
  const asJson = wantsJson(args);
2879
6008
  const verify = hasFlag(args, '--verify');
6009
+ const verifyOverride = readFlag(args, '--verify', '');
2880
6010
  const completeOnPass = hasFlag(args, '--complete-on-pass');
2881
6011
  const summary = readFlag(args, '--summary', '');
2882
- const ref = stripKnownFlags(args, ['--summary'], ['--json', '--verify', '--complete-on-pass'])[0] || '';
6012
+ const ref = stripKnownFlags(args, ['--summary', '--verify'], ['--json', '--complete-on-pass'])[0] || '';
2883
6013
  let mission = resolveMission(ref);
2884
6014
  if (!mission) {
2885
6015
  exitMissionError(ref ? `Mission "${ref}" not found.` : 'No mission found. Run: atris mission start "..."', 1, asJson);
@@ -2902,6 +6032,7 @@ function tickMission(args) {
2902
6032
  printJsonOrText({ ok: true, action: 'tick_skipped', mission: saved }, [`Skipped ${mission.id}: ${mission.status}`], asJson);
2903
6033
  return;
2904
6034
  }
6035
+ if (returnIfCodexNativeGoalNotStarted(mission, asJson)) return;
2905
6036
 
2906
6037
  // Per the /mission skill design, the calling Claude session IS the per-tick LLM.
2907
6038
  // This CLI subcommand records the tick: writes a structured receipt (matching the
@@ -2914,11 +6045,17 @@ function tickMission(args) {
2914
6045
  const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
2915
6046
  const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
2916
6047
 
6048
+ const effectiveVerifier = effectiveMissionVerifier(mission);
6049
+ const verifierCommand = verify
6050
+ ? String(verifyOverride || effectiveVerifier || '').trim()
6051
+ : '';
6052
+ if (verifierCommand) assertMissionVerifier(verifierCommand, asJson);
6053
+
2917
6054
  let verifierResult = null;
2918
- if (verify && mission.verifier) {
2919
- verifierResult = runVerifier(mission.verifier);
6055
+ if (verify && verifierCommand) {
6056
+ verifierResult = runVerifier(verifierCommand);
2920
6057
  }
2921
- const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: mission.verifier, baseline: worktreeBaseline });
6058
+ const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
2922
6059
 
2923
6060
  // Same layer classification as the run-tick path; manual ticks carry their
2924
6061
  // receipt text in --summary.
@@ -2941,7 +6078,7 @@ function tickMission(args) {
2941
6078
  kind: 'mission_tick',
2942
6079
  tick: tickRecord,
2943
6080
  frozen: {
2944
- verifier: mission.verifier || '',
6081
+ verifier: verifierCommand || effectiveVerifier || '',
2945
6082
  lane: mission.lane || 'workspace',
2946
6083
  started_at: tickStart,
2947
6084
  },
@@ -2950,21 +6087,40 @@ function tickMission(args) {
2950
6087
  worktree: tickWorktree,
2951
6088
  });
2952
6089
 
2953
- let status = 'running';
2954
- let nextAction = mission.verifier ? `run verifier: ${mission.verifier}` : 'attach task, verifier, or proof';
2955
- if (verifierResult?.passed) {
6090
+ let status = 'running';
6091
+ let nextAction = (verifierCommand || effectiveVerifier)
6092
+ ? `run verifier: ${verifierCommand || effectiveVerifier}`
6093
+ : (mission.always_on && missionTaskSpine(mission)?.has_task
6094
+ ? nextCandidateTickAction(mission)
6095
+ : 'attach task, verifier, or proof');
6096
+ const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
6097
+ if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
6098
+ status = 'running';
6099
+ nextAction = missionGoalChainNextAction(nextGoalChain);
6100
+ } else if (verifierResult?.passed) {
2956
6101
  const xpReadyAction = missionXpReadyAction(mission, receiptPath);
2957
- status = (completeOnPass && !mission.always_on && !xpReadyAction) ? 'complete' : 'ready';
6102
+ const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
6103
+ const budgetOpen = budgetRemainingSeconds > 0;
6104
+ status = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen) ? 'complete'
6105
+ : (budgetOpen && !mission.always_on && !xpReadyAction ? 'running' : 'ready');
2958
6106
  nextAction = mission.always_on ? nextCandidateTickAction(mission) :
2959
- (xpReadyAction || (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`));
2960
- } else if (verifierResult) {
2961
- status = 'blocked';
2962
- nextAction = 'fix verifier failure or revise mission';
2963
- }
2964
- const clearsPauseState = !['paused', 'stopped'].includes(status);
2965
- const nextMission = {
2966
- ...mission,
2967
- status,
6107
+ (xpReadyAction
6108
+ || (budgetOpen
6109
+ ? `proof passed with ${durationLabel(budgetRemainingSeconds)} left on the budget; pick the next useful move, then: atris mission tick ${mission.id} --verify --summary "<what changed>"`
6110
+ : (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`)));
6111
+ } else if (verifierResult) {
6112
+ status = 'blocked';
6113
+ nextAction = 'fix verifier failure or revise mission';
6114
+ } else if (nextGoalChain?.pause_ready) {
6115
+ status = 'ready';
6116
+ nextAction = `mission feels good; review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`;
6117
+ } else if (nextGoalChain) {
6118
+ nextAction = missionGoalChainNextAction(nextGoalChain);
6119
+ }
6120
+ const clearsPauseState = !['paused', 'stopped'].includes(status);
6121
+ const nextMission = {
6122
+ ...mission,
6123
+ status,
2968
6124
  paused_at: clearsPauseState ? null : mission.paused_at || null,
2969
6125
  stop_reason: clearsPauseState ? null : mission.stop_reason || null,
2970
6126
  resumed_at: clearsPauseState && mission.status === 'paused' ? tickRecord.finished_at : mission.resumed_at || null,
@@ -2973,11 +6129,12 @@ function tickMission(args) {
2973
6129
  last_tick_status: tickRecord.status,
2974
6130
  last_tick_reason: tickRecord.reason,
2975
6131
  last_tick_index: tickIdx,
2976
- last_tick_layer: tickRecord.layer,
2977
- last_tick_layer_source: tickRecord.layer_source,
2978
- verifier_result: verifierResult || mission.verifier_result || null,
2979
- next_action: nextAction,
2980
- };
6132
+ last_tick_layer: tickRecord.layer,
6133
+ last_tick_layer_source: tickRecord.layer_source,
6134
+ verifier_result: verifierResult || mission.verifier_result || null,
6135
+ ...(nextGoalChain ? { goal_chain: nextGoalChain } : {}),
6136
+ next_action: nextAction,
6137
+ };
2981
6138
  const { mission: saved } = saveMission(nextMission, cwd, 'mission_tick', {
2982
6139
  tick_index: tickIdx, verify, verifier_result: verifierResult, receipt_path: receiptPath, layer: tickRecord.layer,
2983
6140
  });
@@ -2994,15 +6151,12 @@ function tickMission(args) {
2994
6151
  ? seedMissionRunContinuation(saved, cwd, receiptPath)
2995
6152
  : null;
2996
6153
  const outputMission = continuationGoal?.parent || saved;
6154
+ const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: outputMission.id });
2997
6155
  const codexGoalState = refreshCodexGoalController(process.cwd());
2998
6156
  printJsonOrText(
2999
- { ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, receipt_path: receiptPath, log_path: logPath, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
6157
+ { ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, receipt_path: receiptPath, log_path: logPath, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
3000
6158
  [
3001
- `Ticked mission: ${outputMission.objective}`,
3002
- `State: ${outputMission.status}`,
3003
- `Tick: ${tickIdx}`,
3004
- `Next: ${outputMission.next_action}`,
3005
- ...(receiptPath ? [`Receipt: ${receiptPath}`] : []),
6159
+ ...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary),
3006
6160
  ...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
3007
6161
  ],
3008
6162
  asJson,
@@ -3036,7 +6190,14 @@ function receiptShowsPass(receipt) {
3036
6190
  // verifier passed. Mirrors the task plane's proof-only accept guard so the
3037
6191
  // final transition consumes the receipts instead of trusting free text.
3038
6192
  function missionCompletionGate(mission, proof, root = process.cwd()) {
3039
- if (!String(mission.verifier || '').trim()) return { ok: true, source: 'no_verifier' };
6193
+ const remainingSeconds = missionFullBudgetRemainingSeconds(mission);
6194
+ if (remainingSeconds > 0) {
6195
+ return {
6196
+ ok: false,
6197
+ source: 'budget_contract',
6198
+ reason: `full-budget mission still has ${durationLabel(remainingSeconds)} left; keep picking the next useful move or use --force if blocked/unsafe`,
6199
+ };
6200
+ }
3040
6201
  const receipt = readReceiptProof(proof, root);
3041
6202
  if (receipt) {
3042
6203
  if (receipt.mission_id !== mission.id) {
@@ -3047,6 +6208,7 @@ function missionCompletionGate(mission, proof, root = process.cwd()) {
3047
6208
  : { ok: false, source: 'receipt', reason: 'proof receipt does not show a passing verifier' };
3048
6209
  }
3049
6210
  if (mission.verifier_result?.passed === true) return { ok: true, source: 'mission_state' };
6211
+ if (!effectiveMissionVerifier(mission)) return { ok: true, source: 'no_verifier' };
3050
6212
  return { ok: false, source: 'mission_state', reason: 'verifier has not passed for this mission and proof is not a passing receipt' };
3051
6213
  }
3052
6214
 
@@ -3055,6 +6217,7 @@ function completeMission(args) {
3055
6217
  const force = hasFlag(args, '--force');
3056
6218
  const proof = readFlag(args, '--proof', '');
3057
6219
  const ref = stripKnownFlags(args, ['--proof'], ['--json', '--force'])[0] || '';
6220
+ const root = process.cwd();
3058
6221
  if (!ref || !proof) {
3059
6222
  exitMissionError('Usage: atris mission complete <id> --proof "..."', 1, asJson);
3060
6223
  }
@@ -3062,11 +6225,11 @@ function completeMission(args) {
3062
6225
  if (!mission) {
3063
6226
  exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
3064
6227
  }
3065
- const gate = missionCompletionGate(mission, proof, process.cwd());
6228
+ const gate = missionCompletionGate(mission, proof, root);
3066
6229
  if (!gate.ok && !force) {
3067
6230
  exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
3068
6231
  }
3069
- const baselineSummary = pruneMissionWorktreeBaseline(mission, process.cwd());
6232
+ const baselineSummary = pruneMissionWorktreeBaseline(mission, root);
3070
6233
  const completionGate = { ...gate, forced: force && !gate.ok };
3071
6234
  const baseNext = {
3072
6235
  ...mission,
@@ -3079,16 +6242,31 @@ function completeMission(args) {
3079
6242
  };
3080
6243
  const xpNextCommand = missionXpReadyAction(baseNext, proof);
3081
6244
  const completion = missionCompletionReceipt(baseNext, proof, xpNextCommand);
6245
+ const artifactPreview = missionArtifactPaths(baseNext, root);
6246
+ completion.landing.artifact = `Open timeline at ${artifactPreview.relativeIndexHtml}.`;
6247
+ completion.result.artifact = artifactPreview.relativeIndexHtml;
3082
6248
  const next = {
3083
6249
  ...baseNext,
3084
6250
  landing: completion.landing,
3085
6251
  result: completion.result,
3086
6252
  };
3087
- const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof, completion_gate: next.completion_gate });
3088
- const continuationGoal = seedMissionRunContinuation(saved, process.cwd(), proof);
6253
+ const { mission: saved } = saveMission(next, root, 'mission_completed', { proof, completion_gate: next.completion_gate });
6254
+ const continuationGoal = seedMissionRunContinuation(saved, root, proof);
3089
6255
  const outputMission = continuationGoal?.parent || saved;
3090
- const logPath = appendMemberLog(outputMission.owner, 'Mission completed', { mission: outputMission.objective, proof });
3091
- const codexGoalState = refreshCodexGoalController(process.cwd());
6256
+ const artifact = writeMissionArtifact(outputMission, {
6257
+ root,
6258
+ proof,
6259
+ completion,
6260
+ xpNextCommand,
6261
+ continuationGoal,
6262
+ });
6263
+ const logPath = appendMemberLog(outputMission.owner, 'Mission completed', {
6264
+ mission: outputMission.objective,
6265
+ proof,
6266
+ artifact: artifact.index_html,
6267
+ }, root);
6268
+ const atrisGoalState = refreshAtrisGoalController(root, { missionId: continuationGoal?.mission?.id || outputMission.id });
6269
+ const codexGoalState = refreshCodexGoalController(root);
3092
6270
  printJsonOrText(
3093
6271
  {
3094
6272
  ok: true,
@@ -3096,16 +6274,15 @@ function completeMission(args) {
3096
6274
  mission: outputMission,
3097
6275
  landing: completion.landing,
3098
6276
  result: completion.result,
6277
+ artifact,
3099
6278
  log_path: logPath,
6279
+ atris_goal_state: atrisGoalState,
3100
6280
  codex_goal_state: codexGoalState,
3101
6281
  xp_next_command: xpNextCommand,
3102
6282
  continuation_goal: continuationGoal,
3103
6283
  },
3104
6284
  [
3105
- `Completed mission: ${outputMission.objective}`,
3106
6285
  ...missionResultLines(completion),
3107
- `Proof: ${proof}`,
3108
- ...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : []),
3109
6286
  ...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
3110
6287
  ],
3111
6288
  asJson,
@@ -3148,9 +6325,10 @@ function stopMission(args) {
3148
6325
  next_action: status === 'paused' ? `resume with: atris mission tick ${mission.id}` : 'mission stopped',
3149
6326
  };
3150
6327
  const { mission: saved } = saveMission(next, process.cwd(), pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
6328
+ const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id, process.cwd());
3151
6329
  const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason });
3152
6330
  printJsonOrText(
3153
- { ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath },
6331
+ { ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath, direct_goal_request_cleared: directGoalRequestCleared },
3154
6332
  [
3155
6333
  `${pause ? 'Paused' : 'Stopped'} mission: ${saved.objective}`,
3156
6334
  `Reason: ${reason}`,
@@ -3162,8 +6340,50 @@ function stopMission(args) {
3162
6340
 
3163
6341
  function goalMission(args) {
3164
6342
  const asJson = wantsJson(args);
6343
+ if (args[0] === 'ack') {
6344
+ return ackMissionGoal(args.slice(1));
6345
+ }
6346
+ const runtime = String(readFlag(args, '--runtime', 'codex') || 'codex').trim().toLowerCase();
6347
+ if (runtime === 'atris' || runtime === 'atris2' || runtime === 'ax') {
6348
+ const heartbeatMode = hasFlag(args, '--heartbeat');
6349
+ const payload = refreshAtrisGoalController(process.cwd(), { heartbeat: heartbeatMode });
6350
+ if (!payload.goal) {
6351
+ printJsonOrText(payload, ['No active mission found for Atris goal.'], asJson);
6352
+ return;
6353
+ }
6354
+ printJsonOrText(
6355
+ payload,
6356
+ [
6357
+ `Atris goal: ${payload.goal.objective}`,
6358
+ `Runner: ${payload.goal.runner}`,
6359
+ `Next: ${payload.goal.next_command}`,
6360
+ payload.goal.replace_after,
6361
+ ],
6362
+ asJson,
6363
+ );
6364
+ return;
6365
+ }
3165
6366
  const heartbeatMode = hasFlag(args, '--heartbeat');
3166
- const payload = refreshCodexGoalController(process.cwd(), { heartbeat: heartbeatMode });
6367
+ const nativeGoalStatus = readFlag(args, '--native-goal-status', readFlag(args, '--visible-goal-status', ''));
6368
+ const nativeGoalObjective = readFlag(args, '--native-goal-objective', readFlag(args, '--visible-goal-objective', ''));
6369
+ const allowNativeGoalSupersede = hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal');
6370
+ const payload = refreshCodexGoalController(process.cwd(), {
6371
+ heartbeat: heartbeatMode,
6372
+ ...(nativeGoalStatus ? { nativeGoalStatus } : {}),
6373
+ ...(nativeGoalObjective ? { nativeGoalObjective } : {}),
6374
+ ...(allowNativeGoalSupersede ? { allowNativeGoalSupersede: true } : {}),
6375
+ });
6376
+ if (payload.active_goal_conflict) {
6377
+ printJsonOrText(
6378
+ payload,
6379
+ [
6380
+ payload.active_goal_conflict.message,
6381
+ `Next: ${payload.active_goal_conflict.next_command}`,
6382
+ ],
6383
+ asJson,
6384
+ );
6385
+ return;
6386
+ }
3167
6387
  if (!payload.goal) {
3168
6388
  printJsonOrText(
3169
6389
  payload,
@@ -3185,6 +6405,72 @@ function goalMission(args) {
3185
6405
  );
3186
6406
  }
3187
6407
 
6408
+ function ackMissionGoal(args) {
6409
+ const asJson = wantsJson(args);
6410
+ const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json'])[0] || '';
6411
+ if (!ref) {
6412
+ exitMissionError('Usage: atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json', 1, asJson);
6413
+ }
6414
+ const runtime = String(readFlag(args, '--runtime', 'codex') || 'codex').trim().toLowerCase();
6415
+ const status = String(readFlag(args, '--status', 'active') || 'active').trim().toLowerCase();
6416
+ let mission = resolveMission(ref);
6417
+ if (!mission) {
6418
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
6419
+ }
6420
+ if (runtime !== 'codex' || status !== 'active') {
6421
+ exitMissionError('Native goal ack requires --runtime codex --status active.', 2, asJson);
6422
+ }
6423
+
6424
+ const lock = acquireMissionLock(mission.id, process.cwd(), { waitMs: 2000 });
6425
+ if (!lock.ok) {
6426
+ exitMissionError(`[mission goal ack] lock busy (held by pid ${lock.holder?.pid || '?'} since ${lock.holder?.started_at || '?'}). Exit.`, 3, asJson);
6427
+ }
6428
+
6429
+ try {
6430
+ mission = resolveMission(mission.id) || mission;
6431
+ if (!isCodexGoalMission(mission)) {
6432
+ releaseMissionLock(lock);
6433
+ exitMissionError(`Mission "${ref}" uses runner=${mission.runner || 'manual'}; native Codex goal ack is only for runner=codex_goal.`, 2, asJson);
6434
+ }
6435
+ const expectedObjective = codexGoalObjective(mission);
6436
+ const objective = readFlag(args, '--objective', expectedObjective);
6437
+ if (objective !== expectedObjective) {
6438
+ releaseMissionLock(lock);
6439
+ exitMissionError(`Native goal objective mismatch. Expected: ${expectedObjective}`, 2, asJson);
6440
+ }
6441
+ const ack = {
6442
+ runtime: 'codex',
6443
+ status: 'active',
6444
+ objective,
6445
+ acknowledged_at: stampIso(),
6446
+ };
6447
+ const nextMission = {
6448
+ ...mission,
6449
+ native_goal_ack: ack,
6450
+ next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
6451
+ };
6452
+ const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_native_goal_ack', { ack });
6453
+ const payload = refreshCodexGoalController(process.cwd());
6454
+ printJsonOrText(
6455
+ {
6456
+ ok: true,
6457
+ action: 'native_goal_acknowledged',
6458
+ mission: saved,
6459
+ native_goal_ack: ack,
6460
+ codex_goal_state: payload,
6461
+ next_command: payload.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`,
6462
+ },
6463
+ [
6464
+ `Native goal active: ${saved.objective}`,
6465
+ `Next: ${payload.goal?.next_command || saved.next_action}`,
6466
+ ],
6467
+ asJson,
6468
+ );
6469
+ } finally {
6470
+ releaseMissionLock(lock);
6471
+ }
6472
+ }
6473
+
3188
6474
  async function goalLoopMission(args) {
3189
6475
  const asJson = wantsJson(args);
3190
6476
  const noClaude = hasFlag(args, '--no-claude');
@@ -3276,18 +6562,29 @@ atris mission - durable goal + loop + owner + proof state
3276
6562
  default model atris:fast; runner codex_goal publishes the goal for a live
3277
6563
  Codex session to pull via atris mission goal)
3278
6564
  atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
6565
+ atris mission doctor [--local] [--json] Flag no-verifier missions, help missions, stale ready receipts, and blocked always-on loops
3279
6566
  atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
3280
6567
  atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
6568
+ atris mission timeline [id] [--limit <n>] [--all] [--prune-preview] [--write] [--json] Saved landing Changed/Next lines from mission receipts
3281
6569
  atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
3282
6570
  atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
3283
6571
  (rolls up sibling git-worktree missions; --local scopes to this checkout)
3284
- atris mission goal [--heartbeat] [--json]
6572
+ atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json] Create a Mission Room card and shareable receipt from messy intent
6573
+ atris mission prune-runs [--apply] [--days <n>] [--keep-newest <n>] [--json] Compress old run receipts into a manifest and prune unreferenced clutter
6574
+ atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--json]
6575
+ atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
3285
6576
  atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
3286
- atris mission tick <id> [--verify] [--complete-on-pass] [--summary "..."] [--json]
6577
+ atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
3287
6578
  atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
3288
6579
  atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
3289
- [--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--json]
6580
+ [--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede]
6581
+ [--spend-full-budget|--use-whole-budget|--stop-when-done] [--room-preflight|--no-room-preflight]
6582
+ [--room-auto-run|--no-room-auto-run]
6583
+ [--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
3290
6584
  (bare/member-only run prompts; one-word fuzzy intent starts a new visible-goal mission; --due runs the saved queue)
6585
+ (short time like "20 minutes" means finish early; long/sleep time like "5 hours" keeps using the budget; --stop-when-done overrides)
6586
+ (messy, shower, and overnight requests preflight through Mission Room before the visible goal is written)
6587
+ (--room-auto-run makes trusted self-improvement asks preview through Mission Room, select real work, then start one bounded goal)
3291
6588
  (mission-run completions seed the next visible goal: decide and start the next useful mission)
3292
6589
  atris mission complete <id> --proof "..."
3293
6590
  atris mission stop <id> [--pause] [--reason "..."]
@@ -3296,7 +6593,7 @@ Autonomy recipe:
3296
6593
  1. Pick an owner member: atris member create <member> (if missing)
3297
6594
  2. Start a current-agent mission with a verifier:
3298
6595
  atris mission start "ship one proof" --owner <member> --runner codex_goal --lane code --verify "npm test" --stop "verifier passes" --xp-task
3299
- 3. Codex sessions: atris mission goal --json, then mirror goal.visible_goal into the native chat goal
6596
+ 3. Codex sessions: read native get_goal, then pass its status into atris mission goal --native-goal-status <status> --native-goal-objective "<objective>" --json
3300
6597
  Overnight controller: atris mission goal --heartbeat --json
3301
6598
  Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
3302
6599
  4. Do one bounded step, then record it:
@@ -3477,6 +6774,70 @@ function layersMission(args) {
3477
6774
  printJsonOrText({ ok: true, since: sinceRaw || null, total, tagged, untagged, by_layer: byLayer, by_source: bySource, dominant: tagged ? dominant : null, skewed }, lines, asJson);
3478
6775
  }
3479
6776
 
6777
+ function roomMission(args) {
6778
+ const asJson = wantsJson(args);
6779
+ const explicitOwner = readFlag(args, '--owner', '');
6780
+ const input = stripKnownFlags(args, ['--owner'], ['--json', '--room-auto-run', '--no-room-auto-run']).join(' ').trim();
6781
+ if (!input) {
6782
+ exitMissionError('Usage: atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json]', 1, asJson);
6783
+ }
6784
+ const requestedOwner = explicitOwner || process.env.ATRIS_AGENT_ID || '';
6785
+ const ownerResolution = resolveFunctionalOwner({
6786
+ requestedOwner,
6787
+ title: input,
6788
+ note: input,
6789
+ root: process.cwd(),
6790
+ fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
6791
+ });
6792
+ const owner = ownerResolution.owner || requestedOwner || 'mission-lead';
6793
+
6794
+ let room;
6795
+ try {
6796
+ room = buildMissionRoom(input, {
6797
+ owner,
6798
+ root: process.cwd(),
6799
+ ownerResolution,
6800
+ trustedRun: hasFlag(args, '--room-auto-run') && !hasFlag(args, '--no-room-auto-run'),
6801
+ verifier: DEFAULT_LONG_RUN_VERIFIER,
6802
+ });
6803
+ } catch (error) {
6804
+ exitMissionError(error.message || String(error), 1, asJson);
6805
+ }
6806
+ const { receipt, relativePath, room: persistedRoom } = writeMissionRoomReceipt(room, { root: process.cwd() });
6807
+ const payload = {
6808
+ ok: true,
6809
+ action: 'mission_room_created',
6810
+ room: persistedRoom,
6811
+ receipt_path: relativePath,
6812
+ receipt,
6813
+ };
6814
+ printJsonOrText(payload, missionRoomLines(persistedRoom, relativePath), asJson);
6815
+ }
6816
+
6817
+ function pruneRunsMission(args) {
6818
+ const asJson = wantsJson(args);
6819
+ let keepNewest;
6820
+ let keepDays;
6821
+ try {
6822
+ keepNewest = readNonNegativeIntegerFlag(args, '--keep-newest', 200);
6823
+ keepDays = readNonNegativeIntegerFlag(args, '--days', 14);
6824
+ } catch (error) {
6825
+ exitMissionError(error.message || String(error), 1, asJson);
6826
+ }
6827
+ const result = pruneRuns(process.cwd(), {
6828
+ apply: hasFlag(args, '--apply'),
6829
+ archive: !hasFlag(args, '--no-archive'),
6830
+ keepNewest,
6831
+ keepDays,
6832
+ });
6833
+ printJsonOrText(
6834
+ { ok: !result.errors.length, ...result },
6835
+ runsPruneLines(result),
6836
+ asJson,
6837
+ );
6838
+ if (result.errors.length) process.exitCode = 1;
6839
+ }
6840
+
3480
6841
  function missionCommand(args) {
3481
6842
  const subcommand = args[0] || 'status';
3482
6843
  const rest = args.slice(1);
@@ -3488,7 +6849,13 @@ function missionCommand(args) {
3488
6849
  case 'status':
3489
6850
  case 'list':
3490
6851
  case 'ls':
6852
+ case 'show':
6853
+ case 'info':
6854
+ case 'view':
3491
6855
  return statusMission(rest);
6856
+ case 'doctor':
6857
+ case 'check':
6858
+ return doctorMission(rest);
3492
6859
  case 'attach-task':
3493
6860
  case 'ensure-task':
3494
6861
  case 'task-spine':
@@ -3496,10 +6863,19 @@ function missionCommand(args) {
3496
6863
  case 'report':
3497
6864
  case 'debrief':
3498
6865
  return reportMission(rest);
6866
+ case 'timeline':
6867
+ case 'landings':
6868
+ return timelineMission(rest);
3499
6869
  case 'watch':
3500
6870
  return watchMission(rest);
3501
6871
  case 'layers':
3502
6872
  return layersMission(rest);
6873
+ case 'room':
6874
+ return roomMission(rest);
6875
+ case 'prune-runs':
6876
+ case 'runs-prune':
6877
+ case 'clean-runs':
6878
+ return pruneRunsMission(rest);
3503
6879
  case 'goal':
3504
6880
  case 'codex-goal':
3505
6881
  return goalMission(rest);
@@ -3538,6 +6914,7 @@ module.exports = {
3538
6914
  cappedClaudeReceiptText,
3539
6915
  extractLayerFromReceiptText,
3540
6916
  classifyPathsByLayer,
6917
+ collectMissionDoctorFindings,
3541
6918
  resolveClaudeRunnerModel,
3542
6919
  resolveClaudeRunnerBin,
3543
6920
  businessIdForAtris2Mission,