atris 3.44.0 → 3.46.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.
@@ -98,6 +98,7 @@ const GOAL_LOOP_STATUSES = new Set(['planning', 'running', 'ready']);
98
98
  const STATUS_ALIASES = new Set(['active']);
99
99
  const CODEX_NATIVE_GOAL_SLOT_STATUSES = new Set(['active', 'paused', 'usage_limited']);
100
100
  const CODEX_NATIVE_GOAL_REPLACE_STATUSES = new Set(['active', 'paused', 'usage_limited']);
101
+ const CODEX_NATIVE_GOAL_CLOSED_STATUSES = new Set(['complete', 'completed', 'achieved']);
101
102
  const DEFAULT_LONG_RUN_VERIFIER = 'git diff --check';
102
103
  const SLEEP_LENGTH_BUDGET_SECONDS = 3600;
103
104
  const HUMAN_BLOCKING_PAUSE_REASONS = new Set(['auth-required', 'model-unavailable', 'rate-limit-exceeded-wall']);
@@ -6552,9 +6553,14 @@ function normalizeCodexNativeGoalStatus(value) {
6552
6553
  if (compact === 'usagelimited') return 'usage_limited';
6553
6554
  if (compact === 'active') return 'active';
6554
6555
  if (compact === 'paused') return 'paused';
6556
+ if (CODEX_NATIVE_GOAL_CLOSED_STATUSES.has(compact)) return 'complete';
6555
6557
  return raw.toLowerCase();
6556
6558
  }
6557
6559
 
6560
+ function codexRuntimeTaskIsClosed(runtimeGoalState) {
6561
+ return runtimeGoalState?.status === 'complete';
6562
+ }
6563
+
6558
6564
  function codexRuntimeGoalStatusLabel(runtimeGoalState) {
6559
6565
  const status = runtimeGoalState?.status || '';
6560
6566
  if (status === 'usage_limited') return 'usageLimited';
@@ -6652,9 +6658,7 @@ function codexNativeGoalRuntimeReplaceAction(newMission, runtimeGoalState = null
6652
6658
  const toObjective = codexGoalObjective(newMission);
6653
6659
  const fromObjective = runtimeGoalState?.objective || null;
6654
6660
  const ackNewMission = commands.ack_new_mission || codexGoalAckCommand(newMission, toObjective);
6655
- const completeCurrentGoal = 'update_goal({ status: "complete" })';
6656
- const createNewGoal = `create_goal({ objective: ${JSON.stringify(toObjective)} })`;
6657
- const supersedeApproved = commands.allow_native_goal_supersede === true;
6661
+ const newTaskInstruction = `Create a new Codex task for ${JSON.stringify(toObjective)}, then run ${ackNewMission}`;
6658
6662
  return {
6659
6663
  runtime: 'codex',
6660
6664
  tool: 'replace_goal',
@@ -6672,23 +6676,18 @@ function codexNativeGoalRuntimeReplaceAction(newMission, runtimeGoalState = null
6672
6676
  ack_new_mission: ackNewMission,
6673
6677
  },
6674
6678
  fallback: {
6675
- reason: 'This Codex runtime exposes get_goal/create_goal/update_goal but not replace_goal or resume_goal.',
6676
- automatic: supersedeApproved,
6677
- approved: supersedeApproved,
6678
- executable_now: supersedeApproved,
6679
- blocked_by: supersedeApproved ? null : 'native_goal_cancel_or_supersede_tool_missing',
6680
- safe_when: 'Use only when a mission handoff proves the paused goal is intentionally superseded; update_goal complete otherwise misrepresents abandoned work as finished.',
6681
- sequence_name: 'complete_paused_goal_then_create_new_goal',
6682
- sequence: [
6683
- completeCurrentGoal,
6684
- createNewGoal,
6685
- ackNewMission,
6686
- ],
6679
+ reason: 'A different objective belongs in a new Codex task; this task keeps its current goal and history.',
6680
+ automatic: false,
6681
+ approved: false,
6682
+ executable_now: false,
6683
+ blocked_by: 'new_codex_task_required',
6684
+ safe_when: 'Continue here only when the active objective still matches this task.',
6685
+ sequence_name: 'new_codex_task_required',
6686
+ sequence: [],
6687
6687
  commands: {
6688
6688
  ...commands,
6689
- complete_current_goal: completeCurrentGoal,
6690
- create_new_goal: createNewGoal,
6691
- ack_new_mission: ackNewMission,
6689
+ create_new_task: newTaskInstruction,
6690
+ ack_new_mission_after_task_create: ackNewMission,
6692
6691
  },
6693
6692
  },
6694
6693
  };
@@ -6887,14 +6886,28 @@ function codexNativeGoalReplaceInstruction(mission, runtimeGoalState = null, obj
6887
6886
  ? ` from paused objective ${JSON.stringify(runtimeGoalState.objective)}`
6888
6887
  : '';
6889
6888
  if (options.allowNativeGoalSupersede === true) {
6890
- 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.`;
6889
+ return `Same-task supersede refused${fromObjective}. Create a new Codex task for ${JSON.stringify(objective)}, then run ${codexGoalAckCommand(mission, objective)} there.`;
6891
6890
  }
6892
- 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.`;
6891
+ return `This Codex task keeps${fromObjective || ' its current objective'}. Create a new dedicated Codex task for ${JSON.stringify(objective)}, then run ${codexGoalAckCommand(mission, objective)} there.`;
6893
6892
  }
6894
6893
 
6895
6894
  function codexNativeGoalBlockPayload(mission, options = {}) {
6896
6895
  const objective = codexGoalObjective(mission);
6897
6896
  const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
6897
+ if (codexRuntimeTaskIsClosed(runtimeGoalState)) {
6898
+ return {
6899
+ ok: false,
6900
+ code: 'completed_task_closed',
6901
+ mission_id: mission.id,
6902
+ objective,
6903
+ runtime_goal_state: runtimeGoalState,
6904
+ requires_native_goal_start: false,
6905
+ requires_new_task: true,
6906
+ native_goal_action: null,
6907
+ native_goal_ack_command: null,
6908
+ next_action: 'Create a new Codex task for this mission. This completed task must retain its final state.',
6909
+ };
6910
+ }
6898
6911
  const recovery = codexNativeGoalRecovery(mission, runtimeGoalState);
6899
6912
  if (recovery) {
6900
6913
  return {
@@ -6985,30 +6998,32 @@ function codexGoalReplaceAfterInstruction(mission) {
6985
6998
  if (missionBudgetContinuationText(mission)) {
6986
6999
  return 'After each proof, run atris mission goal --json again and keep the matching Codex /goal active until the full budget is used.';
6987
7000
  }
6988
- return 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.';
7001
+ return 'After proof or verifier pass, complete this Codex task and stop. A different objective belongs in a new Codex task.';
6989
7002
  }
6990
7003
 
6991
7004
  function codexVisibleGoalBridge(mission, goalObjective, options = {}) {
6992
7005
  const ack = codexNativeGoalAck(mission, goalObjective);
6993
7006
  const recovery = options.nativeGoalRecovery || null;
7007
+ const completedTaskClosed = options.completedTaskClosed === true;
6994
7008
  return {
6995
7009
  schema: 'atris.visible_chat_goal_bridge.v1',
6996
7010
  runtime: 'codex',
6997
7011
  source: 'atris_mission',
6998
7012
  mission_id: mission.id,
6999
7013
  desired_objective: goalObjective,
7000
- status: ack ? 'active' : (recovery ? 'needs_ack_recovery' : 'needs_runtime_write'),
7014
+ status: completedTaskClosed ? 'completed_task_closed' : (ack ? 'active' : (recovery ? 'needs_ack_recovery' : 'needs_runtime_write')),
7001
7015
  acknowledged_at: ack?.acknowledged_at || null,
7002
7016
  state_file: '.atris/state/codex_goal.json',
7003
7017
  status_file: 'atris/status/codex-goal.md',
7004
7018
  operations: {
7005
7019
  read_current_goal: 'get_goal',
7006
7020
  keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
7007
- create_when_empty_or_completed: ack || recovery ? null : 'create_goal({ objective: goal.objective })',
7021
+ create_when_no_goal: ack || recovery || completedTaskClosed ? null : 'create_goal({ objective: goal.objective })',
7022
+ completed_task_action: completedTaskClosed ? 'stop this task and create a new dedicated Codex task for the next objective' : null,
7008
7023
  ack_existing_matching_goal: recovery?.commands?.ack_current_goal || null,
7009
7024
  handoff_when_usage_limited: recovery?.commands?.handoff_to_fresh_agent || null,
7010
- ack_after_create: codexGoalAckCommand(mission, goalObjective),
7011
- complete_after_proof: codexGoalCompletionInstruction(mission),
7025
+ ack_after_create: completedTaskClosed ? null : codexGoalAckCommand(mission, goalObjective),
7026
+ complete_after_proof: completedTaskClosed ? null : codexGoalCompletionInstruction(mission),
7012
7027
  refresh_on_phase_change: 'atris mission goal --json before continuing changed work',
7013
7028
  refresh_next_candidate: 'atris mission goal --json',
7014
7029
  },
@@ -7016,6 +7031,7 @@ function codexVisibleGoalBridge(mission, goalObjective, options = {}) {
7016
7031
  ...(missionBudgetContinuationText(mission)
7017
7032
  ? ['Keep the matching native goal active until the full budget is used, even when an intermediate verifier passes.']
7018
7033
  : []),
7034
+ 'A completed Codex task stays closed; never create another goal in that task.',
7019
7035
  'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
7020
7036
  'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
7021
7037
  'Do not run mission work for runner=codex_goal until ack_after_create has been recorded.',
@@ -7024,7 +7040,23 @@ function codexVisibleGoalBridge(mission, goalObjective, options = {}) {
7024
7040
  };
7025
7041
  }
7026
7042
 
7027
- function codexGoalToolContract(mission, nativeGoalRecovery = null) {
7043
+ function codexGoalToolContract(mission, nativeGoalRecovery = null, options = {}) {
7044
+ if (options.completedTaskClosed === true) {
7045
+ return {
7046
+ current_policy: 'completed Codex tasks retain their final goal state',
7047
+ read_current_goal: 'get_goal',
7048
+ complete_current_goal: 'already complete; do not mutate this task',
7049
+ select_next_goal: 'create a new Codex task for a different objective',
7050
+ set_next_goal: 'do not call create_goal in this completed task',
7051
+ visible_goal_bridge: 'goal.visible_goal',
7052
+ platform_requirement: 'New work and recurring monitors need their own Codex task.',
7053
+ phase_change_refresh: 'continue only while this task is active and the objective still matches',
7054
+ runtime_tool_sequence: 'get_goal -> if complete, stop this task -> create a new Codex task for the next objective',
7055
+ blocked_without_platform_goal_write: true,
7056
+ requires_new_task: true,
7057
+ mission_id: mission.id,
7058
+ };
7059
+ }
7028
7060
  if (nativeGoalRecovery) {
7029
7061
  return {
7030
7062
  current_policy: 'keep one visible Codex /goal active for the selected Atris mission',
@@ -7045,11 +7077,11 @@ function codexGoalToolContract(mission, nativeGoalRecovery = null) {
7045
7077
  read_current_goal: 'get_goal',
7046
7078
  complete_current_goal: codexGoalCompletionInstruction(mission),
7047
7079
  select_next_goal: 'atris mission goal --json',
7048
- set_next_goal: 'use goal.visible_goal: create_goal({ objective: goal.objective }) when no active goal blocks the slot',
7080
+ set_next_goal: 'use goal.visible_goal only when get_goal reports no goal for this task',
7049
7081
  visible_goal_bridge: 'goal.visible_goal',
7050
- platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
7051
- phase_change_refresh: 'before changed follow-up work, run atris mission goal --json and mirror the returned visible goal',
7052
- 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',
7082
+ platform_requirement: 'Codex runtime must create a new task when a completed task needs a different objective.',
7083
+ phase_change_refresh: 'continue in this task only while its active objective still matches',
7084
+ runtime_tool_sequence: 'get_goal -> if no goal, create_goal({ objective }) -> acknowledge -> do matching work -> update_goal({ status: "complete" }) after proof -> stop this task',
7053
7085
  blocked_without_platform_goal_write: true,
7054
7086
  mission_id: mission.id,
7055
7087
  };
@@ -7092,8 +7124,11 @@ function writeCodexGoalState(payload, root = process.cwd()) {
7092
7124
  if (state.goal.visible_goal) {
7093
7125
  lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
7094
7126
  lines.push(`- visible goal desired: ${state.goal.visible_goal.desired_objective}`);
7095
- if (state.goal.visible_goal.operations.create_when_empty_or_completed) {
7096
- lines.push(`- visible goal create: ${state.goal.visible_goal.operations.create_when_empty_or_completed}`);
7127
+ if (state.goal.visible_goal.operations.create_when_no_goal) {
7128
+ lines.push(`- visible goal create: ${state.goal.visible_goal.operations.create_when_no_goal}`);
7129
+ }
7130
+ if (state.goal.visible_goal.operations.completed_task_action) {
7131
+ lines.push(`- completed task: ${state.goal.visible_goal.operations.completed_task_action}`);
7097
7132
  }
7098
7133
  if (state.goal.visible_goal.operations.ack_existing_matching_goal) {
7099
7134
  lines.push(`- visible goal ack recovery: ${state.goal.visible_goal.operations.ack_existing_matching_goal}`);
@@ -7101,10 +7136,12 @@ function writeCodexGoalState(payload, root = process.cwd()) {
7101
7136
  if (state.goal.visible_goal.operations.handoff_when_usage_limited) {
7102
7137
  lines.push(`- usage-limited handoff: ${state.goal.visible_goal.operations.handoff_when_usage_limited}`);
7103
7138
  }
7104
- const completionLabel = missionBudgetContinuationText(state.mission)
7105
- ? 'visible goal hold'
7106
- : 'visible goal complete';
7107
- lines.push(`- ${completionLabel}: ${state.goal.visible_goal.operations.complete_after_proof}`);
7139
+ if (state.goal.visible_goal.operations.complete_after_proof) {
7140
+ const completionLabel = missionBudgetContinuationText(state.mission)
7141
+ ? 'visible goal hold'
7142
+ : 'visible goal complete';
7143
+ lines.push(`- ${completionLabel}: ${state.goal.visible_goal.operations.complete_after_proof}`);
7144
+ }
7108
7145
  }
7109
7146
  if (state.goal.native_goal_recovery) {
7110
7147
  lines.push(`- native goal recovery: ${state.goal.native_goal_recovery.next_command}`);
@@ -7174,15 +7211,18 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
7174
7211
 
7175
7212
  let { mission } = selected;
7176
7213
  const { reason, direct_goal_request: directGoalRequest, seeded_continuation_goal: seededContinuationGoal } = selected;
7177
- const autoNativeGoalAck = maybeAutoAckCodexNativeGoal(mission, root, options);
7214
+ const completedTaskClosed = codexRuntimeTaskIsClosed(runtimeGoalState);
7215
+ const autoNativeGoalAck = completedTaskClosed ? null : maybeAutoAckCodexNativeGoal(mission, root, options);
7178
7216
  if (autoNativeGoalAck) mission = autoNativeGoalAck.saved;
7179
7217
  const taskSpine = missionTaskSpine(mission);
7180
7218
  const missionView = missionStatusView(mission);
7181
7219
  const objective = codexGoalObjective(mission);
7182
7220
  const ack = codexNativeGoalAck(mission);
7183
- const nativeGoalRecovery = !ack ? codexNativeGoalRecovery(mission, runtimeGoalState, root) : null;
7184
- const runtimeNeedsReplace = !ack && !nativeGoalRecovery && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
7185
- const nativeGoalAction = ack
7221
+ const nativeGoalRecovery = !completedTaskClosed && !ack ? codexNativeGoalRecovery(mission, runtimeGoalState, root) : null;
7222
+ const runtimeNeedsReplace = !completedTaskClosed && !ack && !nativeGoalRecovery && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
7223
+ const nativeGoalAction = completedTaskClosed
7224
+ ? null
7225
+ : ack
7186
7226
  ? null
7187
7227
  : nativeGoalRecovery
7188
7228
  ? null
@@ -7201,7 +7241,9 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
7201
7241
  executed_by: taskSpine?.executed_by || mission.executed_by || null,
7202
7242
  task_spine: taskSpine,
7203
7243
  reason,
7204
- next_command: nativeGoalRecovery
7244
+ next_command: completedTaskClosed
7245
+ ? 'Create a new Codex task for this mission. This completed task must retain its final state.'
7246
+ : nativeGoalRecovery
7205
7247
  ? nativeGoalRecovery.next_command
7206
7248
  : runtimeNeedsReplace
7207
7249
  ? codexNativeGoalReplaceInstruction(mission, runtimeGoalState, objective, {
@@ -7209,11 +7251,12 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
7209
7251
  })
7210
7252
  : codexGoalNextCommand(mission),
7211
7253
  replace_after: codexGoalReplaceAfterInstruction(mission),
7212
- visible_goal: codexVisibleGoalBridge(mission, objective, { nativeGoalRecovery }),
7213
- codex_tool_contract: codexGoalToolContract(mission, nativeGoalRecovery),
7214
- requires_native_goal_start: !ack && !nativeGoalRecovery,
7254
+ visible_goal: codexVisibleGoalBridge(mission, objective, { nativeGoalRecovery, completedTaskClosed }),
7255
+ codex_tool_contract: codexGoalToolContract(mission, nativeGoalRecovery, { completedTaskClosed }),
7256
+ requires_native_goal_start: !completedTaskClosed && !ack && !nativeGoalRecovery,
7215
7257
  requires_native_goal_recovery: Boolean(nativeGoalRecovery),
7216
7258
  requires_native_goal_replace: runtimeNeedsReplace,
7259
+ requires_new_task: completedTaskClosed,
7217
7260
  native_goal_action: nativeGoalAction,
7218
7261
  native_goal_recovery: nativeGoalRecovery,
7219
7262
  native_goal_ack_command: codexGoalAckCommand(mission, objective),
@@ -7230,13 +7273,14 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
7230
7273
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
7231
7274
  return {
7232
7275
  ok: true,
7233
- action: heartbeatMode ? 'codex_goal_heartbeat' : 'codex_goal_candidate',
7276
+ action: completedTaskClosed ? 'completed_task_closed' : (heartbeatMode ? 'codex_goal_heartbeat' : 'codex_goal_candidate'),
7234
7277
  goal,
7235
7278
  mission: missionView,
7236
7279
  heartbeat,
7237
7280
  requires_native_goal_start: goal.requires_native_goal_start,
7238
7281
  requires_native_goal_recovery: goal.requires_native_goal_recovery,
7239
7282
  requires_native_goal_replace: goal.requires_native_goal_replace,
7283
+ requires_new_task: goal.requires_new_task,
7240
7284
  native_goal_action: goal.native_goal_action,
7241
7285
  native_goal_recovery: goal.native_goal_recovery,
7242
7286
  auto_native_goal_ack: goal.auto_native_goal_ack,
@@ -7453,6 +7497,7 @@ function goalLoopNextCommandPlan(goal) {
7453
7497
 
7454
7498
  function shouldRunGoalLoopCommand(heartbeat, plan) {
7455
7499
  if (!heartbeat?.goal) return false;
7500
+ if (heartbeat.action === 'completed_task_closed' || heartbeat.goal.requires_new_task === true) return false;
7456
7501
  if (heartbeat.goal.requires_native_goal_start === true) return false;
7457
7502
  if (plan && plan.run_when_due_only === false) return true;
7458
7503
  return heartbeat.heartbeat?.due === true;
@@ -10409,12 +10454,12 @@ function reapPausedMissions(root = process.cwd(), { hours = MISSION_PAUSED_REAP_
10409
10454
  }
10410
10455
 
10411
10456
  function missionGoalHelp() {
10412
- console.log('Usage: atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused|usageLimited] [--native-goal-objective "..."] [--manual-ack] [--allow-native-goal-supersede] [--json]');
10457
+ console.log('Usage: atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused|usageLimited|complete] [--native-goal-objective "..."] [--manual-ack] [--allow-native-goal-supersede] [--json]');
10413
10458
  console.log('Refresh the visible native goal from active mission state. Help is read-only.');
10414
10459
  }
10415
10460
 
10416
10461
  function missionGoalLoopHelp() {
10417
- console.log('Usage: atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--dry-run] [--once] [--json]');
10462
+ console.log('Usage: atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--native-goal-status active|paused|usageLimited|complete] [--native-goal-objective "..."] [--dry-run] [--once] [--json]');
10418
10463
  console.log('Run the bounded native-goal controller. Help is read-only and never starts due work.');
10419
10464
  }
10420
10465
 
@@ -10470,6 +10515,18 @@ function goalMission(args) {
10470
10515
  );
10471
10516
  return;
10472
10517
  }
10518
+ if (payload.action === 'completed_task_closed') {
10519
+ printJsonOrText(
10520
+ payload,
10521
+ [
10522
+ 'Completed Codex task stays closed.',
10523
+ `Next: ${payload.goal.next_command}`,
10524
+ ],
10525
+ asJson,
10526
+ );
10527
+ process.exitCode = 2;
10528
+ return;
10529
+ }
10473
10530
 
10474
10531
  printJsonOrText(
10475
10532
  payload,
@@ -10566,12 +10623,13 @@ async function goalLoopMission(args) {
10566
10623
  const once = hasFlag(args, '--once');
10567
10624
  const maxIterations = once ? 1 : Math.max(1, Number(readFlag(args, '--max-iterations', '')) || 32);
10568
10625
  const maxWallSeconds = Math.max(1, Number(readFlag(args, '--max-wall', '')) || 8 * 60 * 60);
10626
+ const nativeGoalOptions = codexNativeGoalOptionsFromArgs(args);
10569
10627
  const root = process.cwd();
10570
10628
  const startedAt = Date.now();
10571
10629
  const events = [];
10572
10630
 
10573
10631
  for (let index = 0; index < maxIterations; index += 1) {
10574
- const heartbeat = refreshCodexGoalController(root, { heartbeat: true });
10632
+ const heartbeat = refreshCodexGoalController(root, { heartbeat: true, ...nativeGoalOptions });
10575
10633
  const event = {
10576
10634
  iteration: index + 1,
10577
10635
  heartbeat,
@@ -10597,11 +10655,12 @@ async function goalLoopMission(args) {
10597
10655
  event.run = runMissionGoalNextCommand(root, heartbeat, { noClaude });
10598
10656
  event.ran_heavy_work = event.run.ok === true && event.run.heavy_work === true;
10599
10657
  event.ran_setup_work = event.run.ok === true && event.run.setup_work === true;
10600
- event.after_run = refreshCodexGoalController(root, { heartbeat: true });
10658
+ event.after_run = refreshCodexGoalController(root, { heartbeat: true, ...nativeGoalOptions });
10601
10659
  }
10602
10660
  }
10603
10661
  events.push(event);
10604
10662
 
10663
+ if (heartbeat.action === 'completed_task_closed') break;
10605
10664
  if (index + 1 >= maxIterations) break;
10606
10665
  const elapsedSeconds = (Date.now() - startedAt) / 1000;
10607
10666
  const remainingSeconds = maxWallSeconds - elapsedSeconds;
@@ -10615,9 +10674,10 @@ async function goalLoopMission(args) {
10615
10674
  await sleep(sleepSeconds * 1000);
10616
10675
  }
10617
10676
 
10618
- const finalState = refreshCodexGoalController(root, { heartbeat: true });
10677
+ const finalState = refreshCodexGoalController(root, { heartbeat: true, ...nativeGoalOptions });
10678
+ const completedTaskClosed = finalState.action === 'completed_task_closed';
10619
10679
  const payload = {
10620
- ok: true,
10680
+ ok: !completedTaskClosed,
10621
10681
  action: 'codex_goal_loop',
10622
10682
  iterations: events.length,
10623
10683
  max_iterations: maxIterations,
@@ -10626,6 +10686,8 @@ async function goalLoopMission(args) {
10626
10686
  setup_runs: events.filter((event) => event.ran_setup_work).length,
10627
10687
  events,
10628
10688
  final_state: finalState,
10689
+ status: completedTaskClosed ? 'completed_task_closed' : 'running',
10690
+ next_action: completedTaskClosed ? finalState.goal?.next_command || null : null,
10629
10691
  };
10630
10692
  printJsonOrText(
10631
10693
  payload,
@@ -10637,6 +10699,7 @@ async function goalLoopMission(args) {
10637
10699
  ],
10638
10700
  asJson,
10639
10701
  );
10702
+ if (completedTaskClosed) process.exitCode = 2;
10640
10703
  }
10641
10704
 
10642
10705
  function help() {
@@ -10662,9 +10725,9 @@ atris mission - durable goal + loop + owner + proof state
10662
10725
  (rolls up sibling git-worktree missions; --local scopes to this checkout)
10663
10726
  atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json] Create a Mission Room card and shareable receipt from messy intent
10664
10727
  atris mission prune-runs [--apply] [--days <n>] [--keep-newest <n>] [--json] Compress old run receipts into a manifest and prune unreferenced clutter
10665
- atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused|usageLimited] [--native-goal-objective "..."] [--manual-ack] [--allow-native-goal-supersede] [--json]
10728
+ atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused|usageLimited|complete] [--native-goal-objective "..."] [--manual-ack] [--allow-native-goal-supersede] [--json]
10666
10729
  atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
10667
- atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
10730
+ atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--native-goal-status active|paused|usageLimited|complete] [--native-goal-objective "..."] [--dry-run] [--once] [--json]
10668
10731
  atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--self-drive] [--summary "..."]
10669
10732
  [--native-goal-status active|paused|usageLimited] [--native-goal-objective "..."] [--json]
10670
10733
  atris mission set-runner <id> <runner|engine> [--model <id>] [--json]
package/commands/pack.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const { isUtf8 } = require('buffer');
7
7
  const { createHash } = require('crypto');
8
8
  const { spawnSync } = require('child_process');
9
+ const { version: CLI_VERSION } = require('../package.json');
9
10
  const { apiRequestJson, getApiBaseUrl, getAppBaseUrl, httpRequest } = require('../utils/api');
10
11
  const { loadCredentials, performTokenRefresh } = require('../utils/auth');
11
12
  const { createZipBuffer, readZipBuffer, ZIP_LIMITS } = require('../lib/zip');
@@ -156,6 +157,7 @@ const ENV_ASSIGNMENT = /=(?!=*$)/;
156
157
 
157
158
  function showHelp() {
158
159
  console.log('usage: atris pack craft "<topic>" [--dir <target>] [--force]');
160
+ console.log(' atris pack seal <dir> [--type <t>] [--entrypoint <file>]');
159
161
  console.log(' atris pack publish [--dir atris] [--slug <slug>] [--author "<name>"] [--notes "..."] [--visibility public|unlisted|private] [--minor|--major] [--out <file.zip>] [--push] [--dry-run] [--allow-secrets]');
160
162
  console.log(' atris pack install <file.zip|url|slug> [--dir <target>] [--force]');
161
163
  console.log(' atris pack run <slug|dir> [--dir <target>] [--input <file>] [--cloud] [--force] [--trust] [--grant <capability>]');
@@ -1377,14 +1379,8 @@ async function showPackSales(rawArgs, cwd = process.cwd(), options = {}) {
1377
1379
  // ── pack purchases ──────────────────────────────────────────────────────────
1378
1380
  const PACK_PURCHASES_LOGIN_NUDGE = 'not logged in. run atris login first to view pack purchases.';
1379
1381
 
1380
- function packPurchaseItems(payload) {
1381
- if (Array.isArray(payload)) return payload;
1382
- if (payload && Array.isArray(payload.purchases)) return payload.purchases;
1383
- if (payload && Array.isArray(payload.data)) return payload.data;
1384
- if (payload && payload.data && Array.isArray(payload.data.purchases)) {
1385
- return payload.data.purchases;
1386
- }
1387
- return null;
1382
+ function packPurchasesUrl(apiBaseUrl = getApiBaseUrl()) {
1383
+ return `${String(apiBaseUrl || '').replace(/\/+$/, '')}/pack/purchases/mine`;
1388
1384
  }
1389
1385
 
1390
1386
  async function showPackPurchases(rawArgs, cwd = process.cwd(), options = {}) {
@@ -1393,28 +1389,32 @@ async function showPackPurchases(rawArgs, cwd = process.cwd(), options = {}) {
1393
1389
 
1394
1390
  const deps = options.deps || {};
1395
1391
  const print = options.print || console.log;
1392
+ const request = deps.httpRequest || httpRequest;
1393
+ const authHeaders = requiredAuthHeaders(deps, 'view pack purchases');
1396
1394
 
1397
- let payload;
1395
+ let response;
1398
1396
  try {
1399
- payload = await requestRegistryJson(
1400
- '/api/pack/purchases',
1401
- {
1402
- authPurpose: 'view pack purchases',
1403
- unreachableMessage: 'could not load pack purchases. check your connection and try again.',
1404
- invalidMessage: 'pack purchases returned an invalid response.',
1397
+ const apiBaseUrl = (deps.getApiBaseUrl || getApiBaseUrl)();
1398
+ response = await request(packPurchasesUrl(apiBaseUrl), {
1399
+ method: 'GET',
1400
+ timeoutMs: REGISTRY_TIMEOUT_MS,
1401
+ headers: {
1402
+ Accept: 'application/json',
1403
+ ...authHeaders,
1405
1404
  },
1406
- deps,
1407
- );
1408
- } catch (error) {
1409
- if (error && error.status === 401) throw new Error(PACK_PURCHASES_LOGIN_NUDGE);
1410
- if (error && Number.isFinite(error.status)) {
1411
- throw new Error(`could not load pack purchases (status ${error.status}).`);
1412
- }
1413
- throw error;
1405
+ });
1406
+ } catch {
1407
+ throw new Error('could not load pack purchases. check your connection and try again.');
1408
+ }
1409
+
1410
+ if (response.status === 401) throw new Error(PACK_PURCHASES_LOGIN_NUDGE);
1411
+ if (response.status < 200 || response.status >= 300) {
1412
+ throw new Error(`could not load pack purchases (status ${response.status}).`);
1414
1413
  }
1415
1414
 
1416
- const purchases = packPurchaseItems(payload);
1417
- if (!purchases) throw new Error('pack purchases returned an invalid response.');
1415
+ const parsed = parseJsonBody(response.body);
1416
+ if (!Array.isArray(parsed.data)) throw new Error('pack purchases returned an invalid response.');
1417
+ const purchases = parsed.data;
1418
1418
  if (!purchases.length) {
1419
1419
  print('no purchased packs yet. browse with: atris pack browse');
1420
1420
  return 0;
@@ -3003,6 +3003,70 @@ function inspectInstalledContentHashes(packDir, manifest) {
3003
3003
  };
3004
3004
  }
3005
3005
 
3006
+ function sealPack(rawArgs, cwd = process.cwd()) {
3007
+ const args = [...rawArgs];
3008
+ const requestedType = takeValue(args, '--type');
3009
+ const requestedEntrypoint = takeValue(args, '--entrypoint');
3010
+ const source = args.shift();
3011
+ if (!source) {
3012
+ showHelp();
3013
+ return 2;
3014
+ }
3015
+ if (args.length) throw new Error(`unknown pack seal argument: ${args.join(' ')}`);
3016
+
3017
+ const packDir = path.resolve(cwd, source);
3018
+ const manifest = assertPacketDir(packDir, cwd);
3019
+ const contentFiles = collectInstalledContentFiles(packDir);
3020
+ const setFields = [];
3021
+
3022
+ if (!Object.prototype.hasOwnProperty.call(manifest, 'type')) {
3023
+ manifest.type = requestedType !== null
3024
+ ? requestedType
3025
+ : (contentFiles.size === 1 && contentFiles.has('README.md') ? 'playbook' : 'context');
3026
+ setFields.push(['type', manifest.type]);
3027
+ }
3028
+ if (!Object.prototype.hasOwnProperty.call(manifest, 'entrypoint')) {
3029
+ const entrypoint = requestedEntrypoint !== null
3030
+ ? requestedEntrypoint
3031
+ : (contentFiles.has('README.md') ? 'README.md' : null);
3032
+ if (entrypoint !== null) {
3033
+ manifest.entrypoint = entrypoint;
3034
+ setFields.push(['entrypoint', manifest.entrypoint]);
3035
+ }
3036
+ }
3037
+ if (!Object.prototype.hasOwnProperty.call(manifest, 'permissions')) {
3038
+ manifest.permissions = [];
3039
+ setFields.push(['permissions', '[]']);
3040
+ }
3041
+ if (!Object.prototype.hasOwnProperty.call(manifest, 'created-in')) {
3042
+ manifest['created-in'] = CLI_VERSION;
3043
+ setFields.push(['created-in', manifest['created-in']]);
3044
+ }
3045
+
3046
+ manifest['content-hashes'] = Object.fromEntries(
3047
+ [...contentFiles.entries()]
3048
+ .map(([relativePath, absolutePath]) => {
3049
+ const contentPath = canonicalContentPath(relativePath);
3050
+ if (contentPath !== relativePath) {
3051
+ throw new Error(`pack content-hashes requires canonical file path: ${relativePath}`);
3052
+ }
3053
+ return [contentPath, sha256(fs.readFileSync(absolutePath))];
3054
+ })
3055
+ .sort(([left], [right]) => left.localeCompare(right)),
3056
+ );
3057
+ setFields.push([
3058
+ 'content-hashes',
3059
+ `${contentFiles.size} file${contentFiles.size === 1 ? '' : 's'}`,
3060
+ ]);
3061
+
3062
+ writeJson(path.join(packDir, 'pack.json'), manifest);
3063
+ for (const [field, value] of setFields) console.log(`set ${field}: ${value}`);
3064
+
3065
+ const result = evaluatePackDoctor(packDir, cwd);
3066
+ printPackDoctor(result);
3067
+ return result.ok ? 0 : 1;
3068
+ }
3069
+
3006
3070
  function printContentHashStatus(result) {
3007
3071
  if (result.status === 'absent') {
3008
3072
  console.log(' content hashes: absent (legacy pack, bytes unverified)');
@@ -3942,6 +4006,7 @@ async function run(argv = []) {
3942
4006
  }
3943
4007
  return result;
3944
4008
  }
4009
+ if (subcommand === 'seal') return sealPack(args);
3945
4010
  if (subcommand === 'publish') return await publishPack(args);
3946
4011
  if (subcommand === 'install') return await installPack(args);
3947
4012
  if (subcommand === 'run') return await runPack(args);
package/commands/pull.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { loadCredentials } = require('../utils/auth');
3
+ const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
4
4
  const { apiRequestJson } = require('../utils/api');
5
5
  const { findAllMembers } = require('./member');
6
6
  const { loadConfig } = require('../utils/config');
@@ -358,15 +358,18 @@ async function pullBusiness(slug) {
358
358
  const autoWake = process.argv.includes('--auto-wake');
359
359
  if (autoWake) {
360
360
  const statusResult = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
361
+ abortOnAuthFailure(statusResult);
361
362
  const computerStatus = statusResult.ok && statusResult.data ? statusResult.data.status : 'unknown';
362
363
  if (computerStatus !== 'running' || !(statusResult.data && statusResult.data.endpoint)) {
363
364
  process.stdout.write(' Waking EC2 computer... ');
364
365
  _coldWake = true;
365
- await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
366
+ const wake = await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
367
+ abortOnAuthFailure(wake, true);
366
368
  const wakeStart = Date.now();
367
369
  while (Date.now() - wakeStart < 90000) {
368
370
  await new Promise((r) => setTimeout(r, 3000));
369
371
  const s = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
372
+ abortOnAuthFailure(s, true);
370
373
  if (s.ok && s.data && s.data.status === 'running' && s.data.endpoint) {
371
374
  const elapsed = Math.floor((Date.now() - wakeStart) / 1000);
372
375
  console.log(`awake (${elapsed}s)`);
package/commands/push.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const readline = require('readline');
5
- const { loadCredentials } = require('../utils/auth');
5
+ const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
6
6
  const { apiRequestJson } = require('../utils/api');
7
7
  const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
8
8
  const { loadManifest, saveManifest, buildManifest, computeLocalHashes, isIgnoredSyncPath, filterSyncFiles } = require('../lib/manifest');
@@ -527,15 +527,18 @@ async function pushAtris() {
527
527
  const autoWake = process.argv.includes('--auto-wake');
528
528
  if (autoWake) {
529
529
  const statusResult = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
530
+ abortOnAuthFailure(statusResult);
530
531
  const computerStatus = statusResult.ok && statusResult.data ? statusResult.data.status : 'unknown';
531
532
  if (computerStatus !== 'running' || !(statusResult.data && statusResult.data.endpoint)) {
532
533
  process.stdout.write(' Waking EC2 computer... ');
533
534
  _coldWake = true;
534
- await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
535
+ const wake = await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
536
+ abortOnAuthFailure(wake, true);
535
537
  const wakeStart = Date.now();
536
538
  while (Date.now() - wakeStart < 90000) {
537
539
  await new Promise((r) => setTimeout(r, 3000));
538
540
  const s = await apiRequestJson(`/business/${businessId}/ai-computer/status`, { method: 'GET', token: creds.token });
541
+ abortOnAuthFailure(s, true);
539
542
  if (s.ok && s.data && s.data.status === 'running' && s.data.endpoint) {
540
543
  const elapsed = Math.floor((Date.now() - wakeStart) / 1000);
541
544
  console.log(`awake (${elapsed}s)`);