atris 3.30.7 → 3.30.12

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.
@@ -346,6 +346,8 @@ function statePaths(root = process.cwd()) {
346
346
  eventsJsonl: path.join(stateDir, 'mission_events.jsonl'),
347
347
  codexGoalJson: path.join(stateDir, 'codex_goal.json'),
348
348
  codexGoalStatus: path.join(root, 'atris', 'status', 'codex-goal.md'),
349
+ atrisGoalJson: path.join(stateDir, 'atris_goal.json'),
350
+ atrisGoalStatus: path.join(root, 'atris', 'status', 'atris-goal.md'),
349
351
  statusNow: path.join(root, 'atris', 'status', 'now.md'),
350
352
  runsDir: path.join(root, 'atris', 'runs'),
351
353
  };
@@ -899,6 +901,9 @@ function missionRunSmokeVerifier() {
899
901
  "if(p.action!=='mission_run_started')process.exit(2)",
900
902
  "if(p.mission?.runner!=='codex_goal')process.exit(3)",
901
903
  "if(p.codex_goal_state?.goal?.visible_goal?.schema!=='atris.visible_chat_goal_bridge.v1')process.exit(4)",
904
+ "if(p.requires_native_goal_start!==true)process.exit(5)",
905
+ "if(p.native_goal_action?.tool!=='create_goal')process.exit(6)",
906
+ "if(!/create_goal/.test(p.next_command||''))process.exit(7)",
902
907
  ].join(';');
903
908
  return `node -e ${JSON.stringify(script)}`;
904
909
  }
@@ -1004,12 +1009,14 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
1004
1009
 
1005
1010
  const owner = parent.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
1006
1011
  const objective = continuationObjective(parent);
1012
+ const parentRunner = String(parent.runner || '').trim().toLowerCase();
1013
+ const continuationRunner = parentRunner === 'atris2' ? 'atris2' : 'codex_goal';
1007
1014
  const mission = missionFromArgs([
1008
1015
  objective,
1009
1016
  '--owner',
1010
1017
  owner,
1011
1018
  '--runner',
1012
- 'codex_goal',
1019
+ continuationRunner,
1013
1020
  '--lane',
1014
1021
  parent.lane || 'workspace',
1015
1022
  '--cadence',
@@ -1151,7 +1158,11 @@ function startMissionFromRunObjective(objective, args) {
1151
1158
  });
1152
1159
  const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
1153
1160
  const completedContinuationGoal = completeActiveContinuationForStartedMission(saved, process.cwd());
1154
- const codexGoalState = refreshCodexGoalController(process.cwd());
1161
+ const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: saved.id });
1162
+ const codexGoalState = runnerUsesCallerSession(saved.runner)
1163
+ ? refreshCodexGoalController(process.cwd(), { missionId: saved.id })
1164
+ : null;
1165
+ const nativeGoal = codexGoalState?.goal?.requires_native_goal_start ? codexGoalState.goal.native_goal_action : null;
1155
1166
  printJsonOrText(
1156
1167
  {
1157
1168
  ok: true,
@@ -1167,14 +1178,19 @@ function startMissionFromRunObjective(objective, args) {
1167
1178
  dirty_hash: worktreeBaseline.dirty_hash,
1168
1179
  } : null,
1169
1180
  completed_continuation_goal: completedContinuationGoal,
1181
+ atris_goal_state: atrisGoalState,
1170
1182
  codex_goal_state: codexGoalState,
1171
- next_command: codexGoalState.goal?.next_command || `atris mission tick ${saved.id} --verify`,
1183
+ requires_native_goal_start: codexGoalState?.goal?.requires_native_goal_start === true,
1184
+ native_goal_action: nativeGoal,
1185
+ native_goal_ack_command: codexGoalState?.goal?.native_goal_ack_command || null,
1186
+ next_command: codexGoalState?.goal?.next_command || atrisGoalState.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`,
1172
1187
  },
1173
1188
  [
1174
1189
  `Started mission: ${saved.objective}`,
1175
1190
  `Owner: ${saved.owner}`,
1176
1191
  `Runner: ${saved.runner}`,
1177
- ...(codexGoalState.goal ? [`Visible goal: ${codexGoalState.goal.objective}`] : []),
1192
+ ...(atrisGoalState.goal ? [`Atris goal: ${atrisGoalState.goal.objective}`] : []),
1193
+ ...(codexGoalState?.goal ? [`Codex goal: ${codexGoalState.goal.objective}`] : []),
1178
1194
  ...warnings.map((warning) => `Warning: ${warning.message}`),
1179
1195
  ],
1180
1196
  asJson,
@@ -1721,6 +1737,10 @@ function runnerUsesCallerSession(runner) {
1721
1737
  return new Set(['codex_goal', 'caller_session', 'current_agent']).has(String(runner || '').trim().toLowerCase());
1722
1738
  }
1723
1739
 
1740
+ function isCodexGoalMission(mission) {
1741
+ return String(mission?.runner || '').trim().toLowerCase() === 'codex_goal';
1742
+ }
1743
+
1724
1744
  function nextCandidateTickAction(mission) {
1725
1745
  const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
1726
1746
  return `next move: run atris mission run ${mission.id}${completeFlag}`;
@@ -1826,9 +1846,18 @@ function missionSelectableForCodexGoal(mission, now = new Date()) {
1826
1846
  return true;
1827
1847
  }
1828
1848
 
1829
- function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
1849
+ function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Date()) {
1850
+ const requestedId = String(options.missionId || '').trim();
1830
1851
  const candidates = listMissions(root)
1852
+ .filter((mission) => runnerUsesCallerSession(mission.runner))
1831
1853
  .filter((mission) => missionSelectableForCodexGoal(mission, now));
1854
+ if (requestedId) {
1855
+ const exact = candidates.find((mission) => mission.id === requestedId || mission.id.startsWith(requestedId));
1856
+ if (exact) {
1857
+ const due = exact.verifier && missionDueAt(exact, now);
1858
+ return { mission: exact, reason: due ? 'due' : 'selected' };
1859
+ }
1860
+ }
1832
1861
 
1833
1862
  candidates.sort((a, b) => {
1834
1863
  const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
@@ -1850,11 +1879,91 @@ function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
1850
1879
  return { mission, reason: due ? 'due' : 'active' };
1851
1880
  }
1852
1881
 
1882
+ function selectAtrisGoalMission(root = process.cwd(), options = {}, now = new Date()) {
1883
+ const requestedId = String(options.missionId || '').trim();
1884
+ const runnable = listMissions(root).filter((mission) => missionIsRunnable(mission));
1885
+ if (requestedId) {
1886
+ const exact = runnable.find((mission) => mission.id === requestedId || mission.id.startsWith(requestedId));
1887
+ if (exact) return { mission: exact, reason: 'selected' };
1888
+ }
1889
+ runnable.sort((a, b) => {
1890
+ const aDue = a.verifier && missionDueAt(a, now) ? 1 : 0;
1891
+ const bDue = b.verifier && missionDueAt(b, now) ? 1 : 0;
1892
+ if (aDue !== bDue) return bDue - aDue;
1893
+
1894
+ const aTime = missionSortTime(a);
1895
+ const bTime = missionSortTime(b);
1896
+ return bTime - aTime;
1897
+ });
1898
+ const mission = runnable[0] || null;
1899
+ if (!mission) return null;
1900
+ const due = mission.verifier && missionDueAt(mission, now);
1901
+ return { mission, reason: due ? 'due' : 'active' };
1902
+ }
1903
+
1853
1904
  function codexGoalObjective(mission) {
1854
1905
  return mission.objective;
1855
1906
  }
1856
1907
 
1908
+ function codexNativeGoalAck(mission, objective = codexGoalObjective(mission)) {
1909
+ const ack = mission?.native_goal_ack || null;
1910
+ if (!ack || String(ack.runtime || '').toLowerCase() !== 'codex') return null;
1911
+ if (ack.status !== 'active') return null;
1912
+ if (String(ack.objective || '') !== String(objective || '')) return null;
1913
+ return ack;
1914
+ }
1915
+
1916
+ function codexGoalAckCommand(mission, objective = codexGoalObjective(mission)) {
1917
+ return `atris mission goal ack ${mission.id} --runtime codex --status active --objective ${shellQuote(objective)} --json`;
1918
+ }
1919
+
1920
+ function codexNativeGoalAction(mission, objective = codexGoalObjective(mission)) {
1921
+ return {
1922
+ runtime: 'codex',
1923
+ tool: 'create_goal',
1924
+ args: { objective },
1925
+ };
1926
+ }
1927
+
1928
+ function codexNativeGoalStartInstruction(mission, objective = codexGoalObjective(mission)) {
1929
+ return `Call native Codex create_goal({ objective: ${JSON.stringify(objective)} }), then run ${codexGoalAckCommand(mission, objective)}`;
1930
+ }
1931
+
1932
+ function codexNativeGoalBlockPayload(mission) {
1933
+ const objective = codexGoalObjective(mission);
1934
+ return {
1935
+ ok: false,
1936
+ code: 'native_goal_not_started',
1937
+ mission_id: mission.id,
1938
+ objective,
1939
+ requires_native_goal_start: true,
1940
+ native_goal_action: codexNativeGoalAction(mission, objective),
1941
+ native_goal_ack_command: codexGoalAckCommand(mission, objective),
1942
+ next_action: codexNativeGoalStartInstruction(mission, objective),
1943
+ };
1944
+ }
1945
+
1946
+ function maybeBlockUntilCodexNativeGoalStarted(mission, asJson) {
1947
+ if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return;
1948
+ const payload = codexNativeGoalBlockPayload(mission);
1949
+ if (asJson) console.log(JSON.stringify(payload, null, 2));
1950
+ else console.error(payload.next_action);
1951
+ process.exit(2);
1952
+ }
1953
+
1954
+ function returnIfCodexNativeGoalNotStarted(mission, asJson) {
1955
+ if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return false;
1956
+ const payload = codexNativeGoalBlockPayload(mission);
1957
+ if (asJson) console.log(JSON.stringify(payload, null, 2));
1958
+ else console.error(payload.next_action);
1959
+ process.exitCode = 2;
1960
+ return true;
1961
+ }
1962
+
1857
1963
  function codexGoalNextCommand(mission) {
1964
+ if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
1965
+ return codexNativeGoalStartInstruction(mission);
1966
+ }
1858
1967
  const taskSpine = missionTaskSpine(mission);
1859
1968
  if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) {
1860
1969
  return taskSpine.ensure_task_command;
@@ -1874,19 +1983,22 @@ function codexGoalNextCommand(mission) {
1874
1983
  }
1875
1984
 
1876
1985
  function codexVisibleGoalBridge(mission, goalObjective) {
1986
+ const ack = codexNativeGoalAck(mission, goalObjective);
1877
1987
  return {
1878
1988
  schema: 'atris.visible_chat_goal_bridge.v1',
1879
1989
  runtime: 'codex',
1880
1990
  source: 'atris_mission',
1881
1991
  mission_id: mission.id,
1882
1992
  desired_objective: goalObjective,
1883
- status: 'needs_runtime_write',
1993
+ status: ack ? 'active' : 'needs_runtime_write',
1994
+ acknowledged_at: ack?.acknowledged_at || null,
1884
1995
  state_file: '.atris/state/codex_goal.json',
1885
1996
  status_file: 'atris/status/codex-goal.md',
1886
1997
  operations: {
1887
1998
  read_current_goal: 'get_goal',
1888
1999
  keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
1889
2000
  create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
2001
+ ack_after_create: codexGoalAckCommand(mission, goalObjective),
1890
2002
  complete_after_proof: 'update_goal({ status: "complete" })',
1891
2003
  refresh_on_phase_change: 'atris mission goal --json before continuing changed work',
1892
2004
  refresh_next_candidate: 'atris mission goal --json',
@@ -1894,6 +2006,7 @@ function codexVisibleGoalBridge(mission, goalObjective) {
1894
2006
  guardrails: [
1895
2007
  'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
1896
2008
  'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
2009
+ 'Do not run mission work for runner=codex_goal until ack_after_create has been recorded.',
1897
2010
  'When the work changes phase or objective, refresh the mission goal before continuing.',
1898
2011
  ],
1899
2012
  };
@@ -1909,7 +2022,7 @@ function codexGoalToolContract(mission) {
1909
2022
  visible_goal_bridge: 'goal.visible_goal',
1910
2023
  platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
1911
2024
  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 })',
2025
+ 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
2026
  blocked_without_platform_goal_write: true,
1914
2027
  mission_id: mission.id,
1915
2028
  };
@@ -1972,7 +2085,7 @@ function writeCodexGoalState(payload, root = process.cwd()) {
1972
2085
 
1973
2086
  function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1974
2087
  const heartbeatMode = options.heartbeat === true;
1975
- const selected = selectCodexGoalMission(root);
2088
+ const selected = selectCodexGoalMission(root, options);
1976
2089
  if (!selected) {
1977
2090
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(null, null) : undefined;
1978
2091
  return {
@@ -1987,6 +2100,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1987
2100
  const taskSpine = missionTaskSpine(mission);
1988
2101
  const missionView = missionStatusView(mission);
1989
2102
  const objective = codexGoalObjective(mission);
2103
+ const ack = codexNativeGoalAck(mission, objective);
1990
2104
  const goal = {
1991
2105
  objective,
1992
2106
  mission_id: mission.id,
@@ -2000,6 +2114,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
2000
2114
  replace_after: 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.',
2001
2115
  visible_goal: codexVisibleGoalBridge(mission, objective),
2002
2116
  codex_tool_contract: codexGoalToolContract(mission),
2117
+ requires_native_goal_start: !ack,
2118
+ native_goal_action: ack ? null : codexNativeGoalAction(mission, objective),
2119
+ native_goal_ack_command: codexGoalAckCommand(mission, objective),
2120
+ native_goal_ack: ack,
2003
2121
  };
2004
2122
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
2005
2123
  return {
@@ -2008,6 +2126,8 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
2008
2126
  goal,
2009
2127
  mission: missionView,
2010
2128
  heartbeat,
2129
+ requires_native_goal_start: goal.requires_native_goal_start,
2130
+ native_goal_action: goal.native_goal_action,
2011
2131
  };
2012
2132
  }
2013
2133
 
@@ -2021,6 +2141,138 @@ function refreshCodexGoalController(root = process.cwd(), options = {}) {
2021
2141
  };
2022
2142
  }
2023
2143
 
2144
+ function atrisVisibleGoalBridge(mission, goalObjective) {
2145
+ return {
2146
+ schema: 'atris.visible_goal_bridge.v1',
2147
+ runtime: String(mission.runner || 'manual'),
2148
+ source: 'atris_mission',
2149
+ mission_id: mission.id,
2150
+ desired_objective: goalObjective,
2151
+ status: 'active',
2152
+ state_file: '.atris/state/atris_goal.json',
2153
+ status_file: 'atris/status/atris-goal.md',
2154
+ operations: {
2155
+ read_current_goal: 'atris mission goal --runtime atris --json',
2156
+ update_from_mission: 'atris mission tick <mission-id> --summary "<what changed>" --json',
2157
+ refresh_on_phase_change: 'atris mission goal --runtime atris --json before continuing changed work',
2158
+ complete_after_proof: 'atris mission complete <mission-id> --proof "<receipt_path>" --json',
2159
+ },
2160
+ guardrails: [
2161
+ 'Atris owns this goal state; no external native-goal tool ack is required.',
2162
+ 'Do not claim completion until the mission has proof or a recorded blocked human ask.',
2163
+ 'Use the mission task spine when task_spine.current_step_command is present.',
2164
+ ],
2165
+ };
2166
+ }
2167
+
2168
+ function atrisGoalToolContract(mission) {
2169
+ return {
2170
+ current_policy: 'keep one Atris-owned visible goal active for mission/chat/fast runtimes',
2171
+ read_current_goal: 'atris mission goal --runtime atris --json',
2172
+ set_next_goal: 'atris mission run "<objective>" --runner atris2|manual|claude',
2173
+ complete_current_goal: 'atris mission complete <mission-id> --proof "<receipt_path>" --json',
2174
+ visible_goal_bridge: 'goal.visible_goal',
2175
+ platform_requirement: 'No native platform goal tool is required; Atris mission/task state is the source of truth.',
2176
+ runtime_tool_sequence: 'atris mission run -> atris_goal_state active -> ax fast / atris chat shows goal -> mission tick/task proof -> mission complete',
2177
+ blocked_without_platform_goal_write: false,
2178
+ mission_id: mission.id,
2179
+ };
2180
+ }
2181
+
2182
+ function buildAtrisGoalPayload(root = process.cwd(), options = {}) {
2183
+ const heartbeatMode = options.heartbeat === true;
2184
+ const selected = selectAtrisGoalMission(root, options);
2185
+ if (!selected) {
2186
+ return {
2187
+ ok: true,
2188
+ action: heartbeatMode ? 'atris_goal_heartbeat' : 'no_goal_candidate',
2189
+ mission: null,
2190
+ heartbeat: heartbeatMode ? { heavy_work_performed: false, next_heavy_command: null } : undefined,
2191
+ };
2192
+ }
2193
+
2194
+ const { mission, reason } = selected;
2195
+ const taskSpine = missionTaskSpine(mission);
2196
+ const missionView = missionStatusView(mission);
2197
+ const objective = mission.objective;
2198
+ const goal = {
2199
+ objective,
2200
+ mission_id: mission.id,
2201
+ mission_objective: mission.objective,
2202
+ mission_status: mission.status,
2203
+ owner: taskSpine?.owner || mission.owner,
2204
+ executed_by: taskSpine?.executed_by || mission.executed_by || null,
2205
+ runner: mission.runner || 'manual',
2206
+ model: mission.model || null,
2207
+ task_spine: taskSpine,
2208
+ reason,
2209
+ next_command: codexGoalNextCommand(mission),
2210
+ replace_after: 'After proof or phase change, run atris mission goal --runtime atris --json and continue from the returned next_command.',
2211
+ visible_goal: atrisVisibleGoalBridge(mission, objective),
2212
+ atris_tool_contract: atrisGoalToolContract(mission),
2213
+ requires_native_goal_start: false,
2214
+ native_goal_action: null,
2215
+ };
2216
+ return {
2217
+ ok: true,
2218
+ action: heartbeatMode ? 'atris_goal_heartbeat' : 'atris_goal_candidate',
2219
+ goal,
2220
+ mission: missionView,
2221
+ heartbeat: heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined,
2222
+ requires_native_goal_start: false,
2223
+ native_goal_action: null,
2224
+ };
2225
+ }
2226
+
2227
+ function writeAtrisGoalState(payload, root = process.cwd()) {
2228
+ const paths = statePaths(root);
2229
+ const state = {
2230
+ schema: 'atris.goal_controller.v1',
2231
+ updated_at: stampIso(),
2232
+ ...payload,
2233
+ };
2234
+ fs.mkdirSync(path.dirname(paths.atrisGoalJson), { recursive: true });
2235
+ fs.writeFileSync(paths.atrisGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
2236
+
2237
+ const lines = [
2238
+ '# Atris Goal Controller',
2239
+ '',
2240
+ '<!-- Generated by Atris. Do not hand-edit. -->',
2241
+ '',
2242
+ `- updated: ${state.updated_at}`,
2243
+ `- action: ${state.action}`,
2244
+ ];
2245
+ if (state.goal) {
2246
+ lines.push(`- mission: ${state.goal.mission_id}`);
2247
+ lines.push(`- runner: ${state.goal.runner}`);
2248
+ lines.push(`- status: ${state.goal.mission_status}`);
2249
+ lines.push(`- reason: ${state.goal.reason}`);
2250
+ lines.push(`- objective: ${state.goal.objective}`);
2251
+ lines.push(`- next: ${state.goal.next_command}`);
2252
+ lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
2253
+ } else {
2254
+ lines.push('- mission: none');
2255
+ }
2256
+ lines.push('');
2257
+ fs.mkdirSync(path.dirname(paths.atrisGoalStatus), { recursive: true });
2258
+ fs.writeFileSync(paths.atrisGoalStatus, lines.join('\n'), 'utf8');
2259
+ return {
2260
+ state_path: paths.atrisGoalJson,
2261
+ status_path: paths.atrisGoalStatus,
2262
+ state,
2263
+ };
2264
+ }
2265
+
2266
+ function refreshAtrisGoalController(root = process.cwd(), options = {}) {
2267
+ const payload = buildAtrisGoalPayload(root, options);
2268
+ const rendered = writeAtrisGoalState(payload, root);
2269
+ return {
2270
+ ...payload,
2271
+ state_path: rendered.state_path,
2272
+ status_path: rendered.status_path,
2273
+ };
2274
+ }
2275
+
2024
2276
  function runAtrisMissionJsonCommand(root, args, options = {}) {
2025
2277
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
2026
2278
  const result = spawnSync(process.execPath, [cliPath, ...args], {
@@ -2480,6 +2732,8 @@ async function runMission(args) {
2480
2732
  process.exit(0);
2481
2733
  }
2482
2734
 
2735
+ maybeBlockUntilCodexNativeGoalStarted(mission, asJson);
2736
+
2483
2737
  const preLockRunner = String(mission.runner || '').trim().toLowerCase();
2484
2738
  const preLockCallerSession = runnerUsesCallerSession(mission.runner);
2485
2739
  if (!skipClaude && !preLockCallerSession && preLockRunner !== 'atris2') {
@@ -2520,6 +2774,7 @@ async function runMission(args) {
2520
2774
  console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
2521
2775
  return;
2522
2776
  }
2777
+ if (returnIfCodexNativeGoalNotStarted(mission, asJson)) return;
2523
2778
  if (mission.status === 'paused') {
2524
2779
  mission = saveMission({
2525
2780
  ...mission,
@@ -2849,10 +3104,11 @@ async function runMission(args) {
2849
3104
  elapsed_seconds: (Date.now() - startedAt) / 1000,
2850
3105
  worktree: summaryWorktree,
2851
3106
  });
3107
+ const atrisGoalState = refreshAtrisGoalController(cwd, { missionId: mission.id });
2852
3108
  const codexGoalState = refreshCodexGoalController(cwd);
2853
3109
 
2854
3110
  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 },
3111
+ { 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, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
2856
3112
  [
2857
3113
  `Ran mission ${mission.id}`,
2858
3114
  ` objective: ${mission.objective}`,
@@ -2902,6 +3158,7 @@ function tickMission(args) {
2902
3158
  printJsonOrText({ ok: true, action: 'tick_skipped', mission: saved }, [`Skipped ${mission.id}: ${mission.status}`], asJson);
2903
3159
  return;
2904
3160
  }
3161
+ if (returnIfCodexNativeGoalNotStarted(mission, asJson)) return;
2905
3162
 
2906
3163
  // Per the /mission skill design, the calling Claude session IS the per-tick LLM.
2907
3164
  // This CLI subcommand records the tick: writes a structured receipt (matching the
@@ -2994,9 +3251,10 @@ function tickMission(args) {
2994
3251
  ? seedMissionRunContinuation(saved, cwd, receiptPath)
2995
3252
  : null;
2996
3253
  const outputMission = continuationGoal?.parent || saved;
3254
+ const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: outputMission.id });
2997
3255
  const codexGoalState = refreshCodexGoalController(process.cwd());
2998
3256
  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 },
3257
+ { 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
3258
  [
3001
3259
  `Ticked mission: ${outputMission.objective}`,
3002
3260
  `State: ${outputMission.status}`,
@@ -3088,6 +3346,7 @@ function completeMission(args) {
3088
3346
  const continuationGoal = seedMissionRunContinuation(saved, process.cwd(), proof);
3089
3347
  const outputMission = continuationGoal?.parent || saved;
3090
3348
  const logPath = appendMemberLog(outputMission.owner, 'Mission completed', { mission: outputMission.objective, proof });
3349
+ const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: continuationGoal?.mission?.id || outputMission.id });
3091
3350
  const codexGoalState = refreshCodexGoalController(process.cwd());
3092
3351
  printJsonOrText(
3093
3352
  {
@@ -3097,6 +3356,7 @@ function completeMission(args) {
3097
3356
  landing: completion.landing,
3098
3357
  result: completion.result,
3099
3358
  log_path: logPath,
3359
+ atris_goal_state: atrisGoalState,
3100
3360
  codex_goal_state: codexGoalState,
3101
3361
  xp_next_command: xpNextCommand,
3102
3362
  continuation_goal: continuationGoal,
@@ -3162,6 +3422,29 @@ function stopMission(args) {
3162
3422
 
3163
3423
  function goalMission(args) {
3164
3424
  const asJson = wantsJson(args);
3425
+ if (args[0] === 'ack') {
3426
+ return ackMissionGoal(args.slice(1));
3427
+ }
3428
+ const runtime = String(readFlag(args, '--runtime', 'codex') || 'codex').trim().toLowerCase();
3429
+ if (runtime === 'atris' || runtime === 'atris2' || runtime === 'ax') {
3430
+ const heartbeatMode = hasFlag(args, '--heartbeat');
3431
+ const payload = refreshAtrisGoalController(process.cwd(), { heartbeat: heartbeatMode });
3432
+ if (!payload.goal) {
3433
+ printJsonOrText(payload, ['No active mission found for Atris goal.'], asJson);
3434
+ return;
3435
+ }
3436
+ printJsonOrText(
3437
+ payload,
3438
+ [
3439
+ `Atris goal: ${payload.goal.objective}`,
3440
+ `Runner: ${payload.goal.runner}`,
3441
+ `Next: ${payload.goal.next_command}`,
3442
+ payload.goal.replace_after,
3443
+ ],
3444
+ asJson,
3445
+ );
3446
+ return;
3447
+ }
3165
3448
  const heartbeatMode = hasFlag(args, '--heartbeat');
3166
3449
  const payload = refreshCodexGoalController(process.cwd(), { heartbeat: heartbeatMode });
3167
3450
  if (!payload.goal) {
@@ -3185,6 +3468,59 @@ function goalMission(args) {
3185
3468
  );
3186
3469
  }
3187
3470
 
3471
+ function ackMissionGoal(args) {
3472
+ const asJson = wantsJson(args);
3473
+ const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json'])[0] || '';
3474
+ if (!ref) {
3475
+ exitMissionError('Usage: atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json', 1, asJson);
3476
+ }
3477
+ const runtime = String(readFlag(args, '--runtime', 'codex') || 'codex').trim().toLowerCase();
3478
+ const status = String(readFlag(args, '--status', 'active') || 'active').trim().toLowerCase();
3479
+ const mission = resolveMission(ref);
3480
+ if (!mission) {
3481
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
3482
+ }
3483
+ if (!isCodexGoalMission(mission)) {
3484
+ exitMissionError(`Mission "${ref}" uses runner=${mission.runner || 'manual'}; native Codex goal ack is only for runner=codex_goal.`, 2, asJson);
3485
+ }
3486
+ if (runtime !== 'codex' || status !== 'active') {
3487
+ exitMissionError('Native goal ack requires --runtime codex --status active.', 2, asJson);
3488
+ }
3489
+ const expectedObjective = codexGoalObjective(mission);
3490
+ const objective = readFlag(args, '--objective', expectedObjective);
3491
+ if (objective !== expectedObjective) {
3492
+ exitMissionError(`Native goal objective mismatch. Expected: ${expectedObjective}`, 2, asJson);
3493
+ }
3494
+ const ack = {
3495
+ runtime: 'codex',
3496
+ status: 'active',
3497
+ objective,
3498
+ acknowledged_at: stampIso(),
3499
+ };
3500
+ const nextMission = {
3501
+ ...mission,
3502
+ native_goal_ack: ack,
3503
+ next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
3504
+ };
3505
+ const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_native_goal_ack', { ack });
3506
+ const payload = refreshCodexGoalController(process.cwd());
3507
+ printJsonOrText(
3508
+ {
3509
+ ok: true,
3510
+ action: 'native_goal_acknowledged',
3511
+ mission: saved,
3512
+ native_goal_ack: ack,
3513
+ codex_goal_state: payload,
3514
+ next_command: payload.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`,
3515
+ },
3516
+ [
3517
+ `Native goal active: ${saved.objective}`,
3518
+ `Next: ${payload.goal?.next_command || saved.next_action}`,
3519
+ ],
3520
+ asJson,
3521
+ );
3522
+ }
3523
+
3188
3524
  async function goalLoopMission(args) {
3189
3525
  const asJson = wantsJson(args);
3190
3526
  const noClaude = hasFlag(args, '--no-claude');
@@ -3281,7 +3617,8 @@ atris mission - durable goal + loop + owner + proof state
3281
3617
  atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
3282
3618
  atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
3283
3619
  (rolls up sibling git-worktree missions; --local scopes to this checkout)
3284
- atris mission goal [--heartbeat] [--json]
3620
+ atris mission goal [--runtime codex|atris] [--heartbeat] [--json]
3621
+ atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
3285
3622
  atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
3286
3623
  atris mission tick <id> [--verify] [--complete-on-pass] [--summary "..."] [--json]
3287
3624
  atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
@@ -3296,7 +3633,7 @@ Autonomy recipe:
3296
3633
  1. Pick an owner member: atris member create <member> (if missing)
3297
3634
  2. Start a current-agent mission with a verifier:
3298
3635
  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
3636
+ 3. Codex sessions: atris mission goal --json, create the native goal, then run atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
3300
3637
  Overnight controller: atris mission goal --heartbeat --json
3301
3638
  Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
3302
3639
  4. Do one bounded step, then record it: