atris 3.34.0 → 3.35.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.
- package/AGENTS.md +0 -2
- package/FOR_AGENTS.md +5 -3
- package/atris/skills/engines/SKILL.md +16 -7
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris.md +4 -2
- package/ax +475 -17
- package/bin/atris.js +197 -44
- package/commands/aeo.js +52 -0
- package/commands/autoland.js +313 -19
- package/commands/business.js +91 -8
- package/commands/codex-goal.js +26 -2
- package/commands/drive.js +187 -0
- package/commands/engine.js +73 -2
- package/commands/feed.js +202 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +262 -3
- package/commands/init.js +13 -1
- package/commands/integrations.js +39 -11
- package/commands/interview.js +143 -0
- package/commands/land.js +114 -13
- package/commands/lesson.js +112 -1
- package/commands/linear.js +38 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +554 -64
- package/commands/now.js +25 -1
- package/commands/radar.js +259 -14
- package/commands/serve.js +54 -0
- package/commands/status.js +50 -5
- package/commands/stripe.js +38 -0
- package/commands/supabase.js +39 -0
- package/commands/task.js +935 -71
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/worktree.js +68 -10
- package/commands/write.js +399 -0
- package/lib/auto-accept-certified.js +70 -19
- package/lib/autoland.js +39 -3
- package/lib/fleet.js +250 -9
- package/lib/memory-view.js +14 -5
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/official-cli-integration.js +174 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/review-integrity.js +147 -0
- package/lib/runner-command.js +23 -0
- package/lib/task-db.js +220 -7
- package/lib/task-proof.js +20 -0
- package/lib/task-receipt.js +93 -0
- package/package.json +1 -1
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/mission.js
CHANGED
|
@@ -8,6 +8,11 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
8
8
|
const {
|
|
9
9
|
resolveClaudeRunnerModel,
|
|
10
10
|
resolveClaudeRunnerBin,
|
|
11
|
+
resolveClaudeRunnerCommandTemplate,
|
|
12
|
+
buildRunnerCommand,
|
|
13
|
+
RUNNER_PROFILE_DEFS,
|
|
14
|
+
RUNNER_PROFILE_ALIASES,
|
|
15
|
+
RUNNER_PROFILE_NAMES,
|
|
11
16
|
} = require('../lib/runner-command');
|
|
12
17
|
const {
|
|
13
18
|
FUNCTIONAL_MEMBER_TOPICS,
|
|
@@ -94,6 +99,76 @@ function readFlag(args, name, fallback = '') {
|
|
|
94
99
|
return fallback;
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
const MISSION_NATIVE_RUNNER_NAMES = Object.freeze(['manual', 'claude', 'atris2', 'codex_goal', 'caller_session', 'current_agent']);
|
|
103
|
+
const MISSION_NATIVE_RUNNER_SET = new Set(MISSION_NATIVE_RUNNER_NAMES);
|
|
104
|
+
|
|
105
|
+
function canonicalEngineName(name) {
|
|
106
|
+
const trimmed = String(name || '').trim();
|
|
107
|
+
if (!trimmed) return '';
|
|
108
|
+
if (RUNNER_PROFILE_DEFS[trimmed]) return trimmed;
|
|
109
|
+
if (RUNNER_PROFILE_ALIASES[trimmed]) return RUNNER_PROFILE_ALIASES[trimmed];
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function knownMissionRunnerText() {
|
|
114
|
+
return `Known runners: ${MISSION_NATIVE_RUNNER_NAMES.join(', ')}. Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveMissionRunnerSelection(value, options = {}) {
|
|
118
|
+
const raw = String(value || '').trim();
|
|
119
|
+
const asJson = options.asJson === true;
|
|
120
|
+
const engineOnly = options.engineOnly === true;
|
|
121
|
+
if (!raw) {
|
|
122
|
+
exitMissionError(`${options.label || 'runner'} is required. ${knownMissionRunnerText()}.`, 2, asJson);
|
|
123
|
+
}
|
|
124
|
+
const engine = canonicalEngineName(raw);
|
|
125
|
+
if (engine) {
|
|
126
|
+
return { requested: raw, runner: engine, engine, kind: 'engine' };
|
|
127
|
+
}
|
|
128
|
+
const runner = raw.toLowerCase();
|
|
129
|
+
if (!engineOnly && MISSION_NATIVE_RUNNER_SET.has(runner)) {
|
|
130
|
+
return { requested: raw, runner, engine: null, kind: 'runner' };
|
|
131
|
+
}
|
|
132
|
+
const noun = engineOnly ? 'engine' : 'runner';
|
|
133
|
+
exitMissionError(`Unknown ${noun} "${raw}". ${knownMissionRunnerText()}.`, 2, asJson);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function runnerModelPatch(runner, model) {
|
|
137
|
+
const explicitModel = String(model || '').trim();
|
|
138
|
+
if (explicitModel) return { model: explicitModel };
|
|
139
|
+
if (String(runner || '').trim().toLowerCase() === 'atris2') return { model: 'atris:fast' };
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function missionRunRuntimeView(mission, runnerOverride = null, modelOverride = '') {
|
|
144
|
+
if (!runnerOverride) return mission;
|
|
145
|
+
const next = {
|
|
146
|
+
...mission,
|
|
147
|
+
runner: runnerOverride.runner,
|
|
148
|
+
run_runner_override: {
|
|
149
|
+
requested: runnerOverride.requested,
|
|
150
|
+
runner: runnerOverride.runner,
|
|
151
|
+
stored_runner: mission.runner || null,
|
|
152
|
+
stored_model: mission.model || null,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
delete next.model;
|
|
156
|
+
Object.assign(next, runnerModelPatch(runnerOverride.runner, modelOverride));
|
|
157
|
+
if (next.model) next.run_runner_override.model = next.model;
|
|
158
|
+
return next;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function applyMissionRunnerProfile(runner) {
|
|
162
|
+
const engine = canonicalEngineName(runner);
|
|
163
|
+
if (!engine) return () => {};
|
|
164
|
+
const previous = process.env.ATRIS_RUNNER_PROFILE;
|
|
165
|
+
process.env.ATRIS_RUNNER_PROFILE = engine;
|
|
166
|
+
return () => {
|
|
167
|
+
if (previous === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
|
|
168
|
+
else process.env.ATRIS_RUNNER_PROFILE = previous;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
97
172
|
function exitMissionError(message, code = 1, asJson = false) {
|
|
98
173
|
if (asJson) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
99
174
|
else console.error(message);
|
|
@@ -215,6 +290,7 @@ const MISSION_RUN_VALUE_FLAGS = [
|
|
|
215
290
|
'--verify',
|
|
216
291
|
'--stop',
|
|
217
292
|
'--model',
|
|
293
|
+
'--engine',
|
|
218
294
|
'--native-goal-status',
|
|
219
295
|
'--native-goal-objective',
|
|
220
296
|
'--visible-goal-status',
|
|
@@ -233,10 +309,13 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
|
233
309
|
'--spend-full-budget',
|
|
234
310
|
'--use-whole-budget',
|
|
235
311
|
'--stop-when-done',
|
|
312
|
+
'--preflight',
|
|
313
|
+
'--no-preflight',
|
|
236
314
|
'--room-preflight',
|
|
237
315
|
'--no-room-preflight',
|
|
238
316
|
'--room-auto-run',
|
|
239
317
|
'--no-room-auto-run',
|
|
318
|
+
'--manual-ack',
|
|
240
319
|
'--allow-native-goal-supersede',
|
|
241
320
|
'--supersede-paused-native-goal',
|
|
242
321
|
'--take-goal-slot',
|
|
@@ -594,6 +673,8 @@ function exitMissingMission(ref, code = 1, asJson = false) {
|
|
|
594
673
|
if (hints.length) {
|
|
595
674
|
console.error(`Workspace hint: mission ${hints[0].id} exists in ${hints[0].workspace_root}.`);
|
|
596
675
|
console.error(`Run: ${hints[0].command}`);
|
|
676
|
+
} else {
|
|
677
|
+
console.error('Mission ids are workspace-local — run this from the workspace that created the mission.');
|
|
597
678
|
}
|
|
598
679
|
}
|
|
599
680
|
process.exit(code);
|
|
@@ -675,6 +756,14 @@ function memberMissionFile(owner, root = process.cwd()) {
|
|
|
675
756
|
return path.join(dir, 'MISSION.md');
|
|
676
757
|
}
|
|
677
758
|
|
|
759
|
+
function missingOwnerMemberWarning(owner, root = process.cwd()) {
|
|
760
|
+
if (!owner || fs.existsSync(path.join(root, 'atris', 'team', owner))) return null;
|
|
761
|
+
return {
|
|
762
|
+
code: 'missing_owner_member',
|
|
763
|
+
message: `owner "${owner}" has no atris/team/${owner}/ member. Create it (atris member create ${owner}) or pick an existing member (ls atris/team/).`,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
678
767
|
function ensureMemberMissionFile(owner, root = process.cwd(), objective = '') {
|
|
679
768
|
const missionPath = memberMissionFile(owner, root);
|
|
680
769
|
if (!missionPath || fs.existsSync(missionPath)) return missionPath;
|
|
@@ -1075,7 +1164,8 @@ function normalizeMissionReceiptResult(mission, result, receiptPath = '') {
|
|
|
1075
1164
|
}
|
|
1076
1165
|
|
|
1077
1166
|
function missionRunStartNextLine(mission, nextCommand, warnings = []) {
|
|
1078
|
-
|
|
1167
|
+
const missingVerifier = warnings.some((warning) => warning && warning.code === 'missing_verifier');
|
|
1168
|
+
if (missingVerifier) return 'Add a verifier before completion, then run the first proof tick.';
|
|
1079
1169
|
if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
|
|
1080
1170
|
return 'Start the visible goal, then continue this mission.';
|
|
1081
1171
|
}
|
|
@@ -1105,9 +1195,11 @@ function missionRunPreflightSignals(text) {
|
|
|
1105
1195
|
}
|
|
1106
1196
|
|
|
1107
1197
|
function shouldMissionRunRoomPreflight(objective, args = []) {
|
|
1198
|
+
if (hasFlag(args, '--no-preflight')) return false;
|
|
1108
1199
|
if (hasFlag(args, '--no-room-preflight')) return false;
|
|
1200
|
+
if (hasFlag(args, '--preflight')) return true;
|
|
1109
1201
|
if (hasFlag(args, '--room-preflight')) return true;
|
|
1110
|
-
return
|
|
1202
|
+
return true;
|
|
1111
1203
|
}
|
|
1112
1204
|
|
|
1113
1205
|
function missionRunTrustedRoomSignals(text) {
|
|
@@ -1169,6 +1261,8 @@ function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
|
|
|
1169
1261
|
if (!shouldMissionRunRoomPreflight(rawObjective, args)) return null;
|
|
1170
1262
|
const root = options.root || process.cwd();
|
|
1171
1263
|
const owner = options.owner || readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
1264
|
+
const explicitPreflight = hasFlag(args, '--preflight') || hasFlag(args, '--room-preflight');
|
|
1265
|
+
const signalPreflight = missionRunPreflightSignals(rawObjective) || missionRunTrustedRoomSignals(rawObjective);
|
|
1172
1266
|
const trustedRun = options.allowTrustedRun !== false && shouldMissionRunTrustedRoom(rawObjective, args);
|
|
1173
1267
|
const selectedTarget = trustedRun ? selectMissionRunUsefulTarget(rawObjective, root) : null;
|
|
1174
1268
|
const ownerResolution = resolveFunctionalOwner({
|
|
@@ -1191,6 +1285,7 @@ function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
|
|
|
1191
1285
|
const shapedObjective = trustedRun
|
|
1192
1286
|
? missionRunTrustedObjective(rawObjective, written.room, selectedTarget)
|
|
1193
1287
|
: missionRunPreflightObjective(rawObjective, written.room, ownerResolution.owner);
|
|
1288
|
+
const taskSpineRequired = !selectedTarget && (explicitPreflight || signalPreflight);
|
|
1194
1289
|
return {
|
|
1195
1290
|
schema: 'atris.mission_run_preflight.v1',
|
|
1196
1291
|
source: 'mission_room',
|
|
@@ -1209,10 +1304,10 @@ function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
|
|
|
1209
1304
|
ref: selectedTarget.ref || null,
|
|
1210
1305
|
why: selectedTarget.why || '',
|
|
1211
1306
|
} : null,
|
|
1212
|
-
task_spine_required:
|
|
1307
|
+
task_spine_required: taskSpineRequired,
|
|
1213
1308
|
next_action: selectedTarget
|
|
1214
1309
|
? 'run one proof tick for the selected existing task'
|
|
1215
|
-
: 'attach task spine, then run one proof tick',
|
|
1310
|
+
: (taskSpineRequired ? 'attach task spine, then run one proof tick' : 'run one proof tick'),
|
|
1216
1311
|
};
|
|
1217
1312
|
}
|
|
1218
1313
|
|
|
@@ -1613,7 +1708,7 @@ function missionStatusView(mission) {
|
|
|
1613
1708
|
}
|
|
1614
1709
|
|
|
1615
1710
|
function missionFromArgs(args) {
|
|
1616
|
-
const
|
|
1711
|
+
const objectiveParts = stripKnownFlags(args, [
|
|
1617
1712
|
'--owner',
|
|
1618
1713
|
'--cadence',
|
|
1619
1714
|
'--loop',
|
|
@@ -1632,7 +1727,9 @@ function missionFromArgs(args) {
|
|
|
1632
1727
|
'--native-goal-objective',
|
|
1633
1728
|
'--visible-goal-status',
|
|
1634
1729
|
'--visible-goal-objective',
|
|
1635
|
-
], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--duplicate', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--allow-native-goal-supersede', '--supersede-paused-native-goal', '--take-goal-slot'])
|
|
1730
|
+
], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--duplicate', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--preflight', '--no-preflight', '--room-preflight', '--no-room-preflight', '--manual-ack', '--allow-native-goal-supersede', '--supersede-paused-native-goal', '--take-goal-slot'])
|
|
1731
|
+
.filter((part) => String(part || '').trim() !== '...');
|
|
1732
|
+
const objective = objectiveParts.join(' ').trim();
|
|
1636
1733
|
if (!objective) {
|
|
1637
1734
|
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
1638
1735
|
}
|
|
@@ -2808,7 +2905,36 @@ function findActiveTwinMission(objective, owner, root = process.cwd()) {
|
|
|
2808
2905
|
|
|
2809
2906
|
function startMission(args) {
|
|
2810
2907
|
const asJson = wantsJson(args);
|
|
2908
|
+
const firstArg = String(args[0] || '').trim().toLowerCase();
|
|
2909
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || firstArg === 'help') {
|
|
2910
|
+
console.log('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--runner manual|claude|atris2|codex_goal]');
|
|
2911
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
2912
|
+
process.exit(0);
|
|
2913
|
+
}
|
|
2811
2914
|
const mission = missionFromArgs(args);
|
|
2915
|
+
// A flag-looking or empty objective is a typo, not a mission.
|
|
2916
|
+
const rawObjective = String(mission.objective || '').trim();
|
|
2917
|
+
if (!rawObjective || rawObjective.startsWith('-')) {
|
|
2918
|
+
exitMissionError(`mission start needs a quoted objective (got ${rawObjective ? `"${rawObjective}"` : 'nothing'}). Usage: atris mission start "<objective>" --owner <member>`, 1, asJson);
|
|
2919
|
+
}
|
|
2920
|
+
// Pasting a mission id where an objective goes is an id mismatch, not a new
|
|
2921
|
+
// mission: recover the existing record or explain where it actually lives.
|
|
2922
|
+
const idLikeObjective = String(mission.objective || '').trim();
|
|
2923
|
+
if (/^mission-\d{4}-\d{2}-\d{2}-/.test(idLikeObjective)) {
|
|
2924
|
+
const existing = resolveMission(idLikeObjective);
|
|
2925
|
+
if (existing) {
|
|
2926
|
+
printJsonOrText(
|
|
2927
|
+
{ ok: true, action: 'mission_recovered', recovered: true, mission: existing, note: 'objective looked like a mission id; recovered the existing mission instead of creating a new one' },
|
|
2928
|
+
[
|
|
2929
|
+
`That's a mission id, not an objective — recovered ${existing.id} (${existing.status}).`,
|
|
2930
|
+
`Resume: atris mission run ${existing.id}`,
|
|
2931
|
+
],
|
|
2932
|
+
asJson,
|
|
2933
|
+
);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
exitMissingMission(idLikeObjective, 1, asJson);
|
|
2937
|
+
}
|
|
2812
2938
|
if (!hasFlag(args, '--duplicate')) {
|
|
2813
2939
|
const twin = findActiveTwinMission(mission.objective, mission.owner);
|
|
2814
2940
|
if (twin) {
|
|
@@ -2849,7 +2975,7 @@ function startMission(args) {
|
|
|
2849
2975
|
mission.next_action = `work task then run: atris task current-step --goal-id ${mission.id} --as ${mission.owner} --proof "<proof>" --json`;
|
|
2850
2976
|
}
|
|
2851
2977
|
}
|
|
2852
|
-
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
2978
|
+
const warnings = [missingVerifierWarning(mission), missingOwnerMemberWarning(mission.owner)].filter(Boolean);
|
|
2853
2979
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
2854
2980
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective });
|
|
2855
2981
|
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
@@ -2959,7 +3085,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2959
3085
|
mission.xp_task = xpTask;
|
|
2960
3086
|
mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
|
|
2961
3087
|
}
|
|
2962
|
-
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
3088
|
+
const warnings = [missingVerifierWarning(mission), missingOwnerMemberWarning(mission.owner)].filter(Boolean);
|
|
2963
3089
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
2964
3090
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
|
|
2965
3091
|
const directGoalRequest = writeDirectRunCodexGoalRequest(saved, process.cwd());
|
|
@@ -2978,6 +3104,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2978
3104
|
const nativeGoalOptions = {
|
|
2979
3105
|
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
2980
3106
|
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
3107
|
+
...(hasFlag(args, '--manual-ack') ? { manualAck: true } : {}),
|
|
2981
3108
|
...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
|
|
2982
3109
|
};
|
|
2983
3110
|
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
@@ -2987,6 +3114,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2987
3114
|
const codexGoalState = runnerUsesCallerSession(saved.runner)
|
|
2988
3115
|
? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
|
|
2989
3116
|
: null;
|
|
3117
|
+
const outputMission = resolveMission(saved.id, process.cwd()) || saved;
|
|
2990
3118
|
const nativeGoal = codexGoalState?.native_goal_action
|
|
2991
3119
|
|| (codexGoalState?.goal?.requires_native_goal_start ? codexGoalState.goal.native_goal_action : null);
|
|
2992
3120
|
const nextCommand = codexGoalState?.next_command || codexGoalState?.goal?.next_command || atrisGoalState.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`;
|
|
@@ -2994,7 +3122,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2994
3122
|
{
|
|
2995
3123
|
ok: true,
|
|
2996
3124
|
action: 'mission_run_started',
|
|
2997
|
-
mission:
|
|
3125
|
+
mission: outputMission,
|
|
2998
3126
|
budget_contract: saved.budget_contract || null,
|
|
2999
3127
|
warnings,
|
|
3000
3128
|
state_path: statePaths().missionsJsonl,
|
|
@@ -3016,7 +3144,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
3016
3144
|
native_goal_ack_command: codexGoalState?.goal?.native_goal_ack_command || codexGoalState?.active_goal_conflict?.commands?.ack_new_mission || null,
|
|
3017
3145
|
next_command: nextCommand,
|
|
3018
3146
|
},
|
|
3019
|
-
missionRunTakeoffLines(
|
|
3147
|
+
missionRunTakeoffLines(outputMission, { warnings, nextCommand }),
|
|
3020
3148
|
asJson,
|
|
3021
3149
|
);
|
|
3022
3150
|
}
|
|
@@ -3151,6 +3279,74 @@ function attachMissionTask(args) {
|
|
|
3151
3279
|
}
|
|
3152
3280
|
}
|
|
3153
3281
|
|
|
3282
|
+
function setMissionRunner(args) {
|
|
3283
|
+
const asJson = wantsJson(args);
|
|
3284
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
3285
|
+
console.log('Usage: atris mission set-runner <id> <runner|engine> [--model <id>] [--json]');
|
|
3286
|
+
console.log(knownMissionRunnerText());
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const positionals = stripKnownFlags(args, ['--model'], ['--json']);
|
|
3290
|
+
const ref = String(positionals[0] || '').trim();
|
|
3291
|
+
const runnerArg = String(positionals[1] || '').trim();
|
|
3292
|
+
if (!ref || !runnerArg) {
|
|
3293
|
+
exitMissionError(`Usage: atris mission set-runner <id> <runner|engine> [--model <id>]. ${knownMissionRunnerText()}.`, 1, asJson);
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
const selection = resolveMissionRunnerSelection(runnerArg, { asJson, label: 'runner' });
|
|
3297
|
+
const model = readFlag(args, '--model', '');
|
|
3298
|
+
let mission = resolveMission(ref);
|
|
3299
|
+
if (!mission) exitMissingMission(ref, 1, asJson);
|
|
3300
|
+
|
|
3301
|
+
const lock = acquireMissionLock(mission.id);
|
|
3302
|
+
if (!lock.ok) {
|
|
3303
|
+
exitMissionError(`[mission set-runner] lock busy (held by pid ${lock.holder?.pid || '?'} since ${lock.holder?.started_at || '?'}). Exit.`, 3, asJson);
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
try {
|
|
3307
|
+
mission = resolveMission(mission.id) || mission;
|
|
3308
|
+
const previous = { runner: mission.runner || null, model: mission.model || null };
|
|
3309
|
+
const nextMission = {
|
|
3310
|
+
...mission,
|
|
3311
|
+
runner: selection.runner,
|
|
3312
|
+
next_action: mission.next_action || `run: atris mission run ${mission.id}`,
|
|
3313
|
+
};
|
|
3314
|
+
delete nextMission.model;
|
|
3315
|
+
Object.assign(nextMission, runnerModelPatch(selection.runner, model));
|
|
3316
|
+
|
|
3317
|
+
const { mission: saved, event } = saveMission(
|
|
3318
|
+
nextMission,
|
|
3319
|
+
process.cwd(),
|
|
3320
|
+
'mission_runner_changed',
|
|
3321
|
+
{
|
|
3322
|
+
previous_runner: previous.runner,
|
|
3323
|
+
previous_model: previous.model,
|
|
3324
|
+
runner: nextMission.runner,
|
|
3325
|
+
requested_runner: selection.requested,
|
|
3326
|
+
kind: selection.kind,
|
|
3327
|
+
model: nextMission.model || null,
|
|
3328
|
+
},
|
|
3329
|
+
);
|
|
3330
|
+
const logPath = appendMemberLog(saved.owner, 'Mission runner changed', {
|
|
3331
|
+
mission: saved.objective,
|
|
3332
|
+
previous_runner: previous.runner || undefined,
|
|
3333
|
+
runner: saved.runner,
|
|
3334
|
+
model: saved.model || undefined,
|
|
3335
|
+
});
|
|
3336
|
+
printJsonOrText(
|
|
3337
|
+
{ ok: true, action: 'mission_runner_changed', mission: saved, event, log_path: logPath },
|
|
3338
|
+
[
|
|
3339
|
+
`Mission runner changed: ${saved.id}`,
|
|
3340
|
+
`Runner: ${previous.runner || 'none'} -> ${saved.runner}${saved.model ? ` (${saved.model})` : ''}`,
|
|
3341
|
+
`Next: atris mission run ${saved.id}`,
|
|
3342
|
+
],
|
|
3343
|
+
asJson,
|
|
3344
|
+
);
|
|
3345
|
+
} finally {
|
|
3346
|
+
releaseMissionLock(lock);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3154
3350
|
function statusMission(args) {
|
|
3155
3351
|
const asJson = wantsJson(args);
|
|
3156
3352
|
const localOnly = hasFlag(args, '--local');
|
|
@@ -4030,6 +4226,12 @@ function missionReceiptHealth(mission, root = process.cwd()) {
|
|
|
4030
4226
|
return { ok: true, reason: 'verifier_passed', receipt_path: receiptPath };
|
|
4031
4227
|
}
|
|
4032
4228
|
|
|
4229
|
+
function missionIsDriveParked(mission) {
|
|
4230
|
+
if (String(mission?.status || '').toLowerCase() !== 'paused') return false;
|
|
4231
|
+
const reason = String(mission?.stop_reason || '').toLowerCase();
|
|
4232
|
+
return reason.startsWith('drive:') && reason.includes('auto-parked');
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4033
4235
|
function collectMissionDoctorFindings(root = process.cwd(), options = {}) {
|
|
4034
4236
|
const localOnly = options.localOnly === true;
|
|
4035
4237
|
let missions = listMissions(root);
|
|
@@ -4061,7 +4263,10 @@ function collectMissionDoctorFindings(root = process.cwd(), options = {}) {
|
|
|
4061
4263
|
const status = String(mission.status || '');
|
|
4062
4264
|
const active = !TERMINAL_STATUSES.has(status);
|
|
4063
4265
|
const objective = String(mission.objective || '').trim();
|
|
4064
|
-
|
|
4266
|
+
// Only drive's explicit auto-park reason settles a no-verifier mission.
|
|
4267
|
+
// Other paused no-verifier missions still need a verifier or a deliberate
|
|
4268
|
+
// park, otherwise doctor and drive silently disagree about stale work.
|
|
4269
|
+
if (active && !missionIsDriveParked(mission) && !effectiveMissionVerifier(mission)) {
|
|
4065
4270
|
add(
|
|
4066
4271
|
mission,
|
|
4067
4272
|
'missing_verifier',
|
|
@@ -4231,6 +4436,12 @@ function resolveVerifierCommand(command) {
|
|
|
4231
4436
|
return `${leading}${shellQuote(process.execPath)} ${shellQuote(cliPath)}${trimmed.slice('atris'.length)}`;
|
|
4232
4437
|
}
|
|
4233
4438
|
|
|
4439
|
+
function missionVerifierTimeoutMs(env = process.env) {
|
|
4440
|
+
const parsed = Number(env.ATRIS_MISSION_VERIFIER_TIMEOUT_MS);
|
|
4441
|
+
if (Number.isFinite(parsed) && parsed >= 1000) return Math.floor(parsed);
|
|
4442
|
+
return 120000;
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4234
4445
|
function runVerifier(command, root = process.cwd()) {
|
|
4235
4446
|
if (!command) return null;
|
|
4236
4447
|
const resolvedCommand = resolveVerifierCommand(command);
|
|
@@ -4238,7 +4449,7 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
4238
4449
|
cwd: root,
|
|
4239
4450
|
shell: true,
|
|
4240
4451
|
encoding: 'utf8',
|
|
4241
|
-
timeout:
|
|
4452
|
+
timeout: missionVerifierTimeoutMs(),
|
|
4242
4453
|
env: process.env,
|
|
4243
4454
|
});
|
|
4244
4455
|
return {
|
|
@@ -4736,10 +4947,55 @@ function codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission) {
|
|
|
4736
4947
|
return runtimeGoalState.objective === codexGoalObjective(mission);
|
|
4737
4948
|
}
|
|
4738
4949
|
|
|
4950
|
+
function codexRuntimeStateCanAutoAckMission(runtimeGoalState, mission) {
|
|
4951
|
+
if (!runtimeGoalState || !mission) return false;
|
|
4952
|
+
if (runtimeGoalState.status !== 'active') return false;
|
|
4953
|
+
if (!runtimeGoalState.objective) return false;
|
|
4954
|
+
return codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission);
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4739
4957
|
function codexGoalAckCommand(mission, objective = codexGoalObjective(mission)) {
|
|
4740
4958
|
return `atris mission goal ack ${mission.id} --runtime codex --status active --objective ${shellQuote(objective)} --json`;
|
|
4741
4959
|
}
|
|
4742
4960
|
|
|
4961
|
+
function recordCodexNativeGoalAck(mission, root = process.cwd(), options = {}) {
|
|
4962
|
+
const canonicalObjective = codexGoalObjective(mission);
|
|
4963
|
+
const reportedObjective = String(options.reportedObjective || canonicalObjective).trim();
|
|
4964
|
+
const ack = {
|
|
4965
|
+
runtime: 'codex',
|
|
4966
|
+
status: 'active',
|
|
4967
|
+
mission_id: mission.id,
|
|
4968
|
+
objective: canonicalObjective,
|
|
4969
|
+
...(reportedObjective && reportedObjective !== canonicalObjective ? { reported_objective: reportedObjective } : {}),
|
|
4970
|
+
acknowledged_at: stampIso(),
|
|
4971
|
+
};
|
|
4972
|
+
const nextMission = {
|
|
4973
|
+
...mission,
|
|
4974
|
+
native_goal_ack: ack,
|
|
4975
|
+
next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
|
|
4976
|
+
};
|
|
4977
|
+
const supersededNativeGoalAcks = supersedeOtherCodexNativeGoalAcks(root, nextMission, ack);
|
|
4978
|
+
const { mission: saved } = saveMission(
|
|
4979
|
+
nextMission,
|
|
4980
|
+
root,
|
|
4981
|
+
options.eventName || 'mission_native_goal_ack',
|
|
4982
|
+
{ ack, ...(options.eventDetail || {}) },
|
|
4983
|
+
);
|
|
4984
|
+
return { saved, ack, supersededNativeGoalAcks };
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
function maybeAutoAckCodexNativeGoal(mission, root = process.cwd(), options = {}) {
|
|
4988
|
+
if (options.manualAck === true) return null;
|
|
4989
|
+
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return null;
|
|
4990
|
+
const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
|
|
4991
|
+
if (!codexRuntimeStateCanAutoAckMission(runtimeGoalState, mission)) return null;
|
|
4992
|
+
return recordCodexNativeGoalAck(mission, root, {
|
|
4993
|
+
reportedObjective: runtimeGoalState.objective,
|
|
4994
|
+
eventName: 'mission_native_goal_auto_ack',
|
|
4995
|
+
eventDetail: { runtime_goal_state: runtimeGoalState },
|
|
4996
|
+
});
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4743
4999
|
function codexNativeGoalReplaceAction(newMission, activeMission, runtimeGoalState = null, commands = {}) {
|
|
4744
5000
|
const fromObjective = codexGoalObjective(activeMission);
|
|
4745
5001
|
const toObjective = codexGoalObjective(newMission);
|
|
@@ -5010,16 +5266,18 @@ function codexNativeGoalBlockPayload(mission) {
|
|
|
5010
5266
|
};
|
|
5011
5267
|
}
|
|
5012
5268
|
|
|
5013
|
-
function maybeBlockUntilCodexNativeGoalStarted(mission, asJson) {
|
|
5269
|
+
function maybeBlockUntilCodexNativeGoalStarted(mission, asJson, options = {}) {
|
|
5014
5270
|
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return;
|
|
5271
|
+
if (options.manualAck === false) return;
|
|
5015
5272
|
const payload = codexNativeGoalBlockPayload(mission);
|
|
5016
5273
|
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
5017
5274
|
else console.error(payload.next_action);
|
|
5018
5275
|
process.exit(2);
|
|
5019
5276
|
}
|
|
5020
5277
|
|
|
5021
|
-
function returnIfCodexNativeGoalNotStarted(mission, asJson) {
|
|
5278
|
+
function returnIfCodexNativeGoalNotStarted(mission, asJson, options = {}) {
|
|
5022
5279
|
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return false;
|
|
5280
|
+
if (options.manualAck === false) return false;
|
|
5023
5281
|
const payload = codexNativeGoalBlockPayload(mission);
|
|
5024
5282
|
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
5025
5283
|
else console.error(payload.next_action);
|
|
@@ -5206,7 +5464,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5206
5464
|
};
|
|
5207
5465
|
}
|
|
5208
5466
|
|
|
5209
|
-
|
|
5467
|
+
let { mission } = selected;
|
|
5468
|
+
const { reason, direct_goal_request: directGoalRequest, seeded_continuation_goal: seededContinuationGoal } = selected;
|
|
5469
|
+
const autoNativeGoalAck = maybeAutoAckCodexNativeGoal(mission, root, options);
|
|
5470
|
+
if (autoNativeGoalAck) mission = autoNativeGoalAck.saved;
|
|
5210
5471
|
const taskSpine = missionTaskSpine(mission);
|
|
5211
5472
|
const missionView = missionStatusView(mission);
|
|
5212
5473
|
const objective = codexGoalObjective(mission);
|
|
@@ -5242,6 +5503,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5242
5503
|
native_goal_action: nativeGoalAction,
|
|
5243
5504
|
native_goal_ack_command: codexGoalAckCommand(mission, objective),
|
|
5244
5505
|
native_goal_ack: ack,
|
|
5506
|
+
auto_native_goal_ack: autoNativeGoalAck ? {
|
|
5507
|
+
native_goal_ack: autoNativeGoalAck.ack,
|
|
5508
|
+
superseded_native_goal_acks: autoNativeGoalAck.supersededNativeGoalAcks,
|
|
5509
|
+
} : null,
|
|
5245
5510
|
direct_goal_request: directGoalRequest || null,
|
|
5246
5511
|
seeded_continuation_goal: seededContinuationGoal || null,
|
|
5247
5512
|
next_action_preview: missionChoosesNextMission(mission) ? chooseNextMissionPreview(mission, root) : (mission.next_action_preview || null),
|
|
@@ -5257,6 +5522,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5257
5522
|
requires_native_goal_start: goal.requires_native_goal_start,
|
|
5258
5523
|
requires_native_goal_replace: goal.requires_native_goal_replace,
|
|
5259
5524
|
native_goal_action: goal.native_goal_action,
|
|
5525
|
+
auto_native_goal_ack: goal.auto_native_goal_ack,
|
|
5260
5526
|
runtime_goal_state: runtimeGoalState,
|
|
5261
5527
|
};
|
|
5262
5528
|
}
|
|
@@ -5561,6 +5827,25 @@ function consecutiveSameReasonErrors(ticks) {
|
|
|
5561
5827
|
return { reason: last.reason, count };
|
|
5562
5828
|
}
|
|
5563
5829
|
|
|
5830
|
+
function isTransientAtris2BackendError(error) {
|
|
5831
|
+
const text = String(error || '');
|
|
5832
|
+
if (!text) return false;
|
|
5833
|
+
return /\b(?:ECONNREFUSED|ECONNRESET|ETIMEDOUT|EPIPE)\b/i.test(text)
|
|
5834
|
+
|| /\b(?:socket hang up|request timeout|no response headers|stream stalled)\b/i.test(text)
|
|
5835
|
+
|| /\bHTTP\s+(?:502|503|504|522|523|524)\b/i.test(text)
|
|
5836
|
+
// A stopped AtrisOS computer answers 409 "Computer must be running": the
|
|
5837
|
+
// backend will come back, so the mission keeps waiting instead of pausing.
|
|
5838
|
+
|| (/\bHTTP\s+409\b/i.test(text) && /computer must be running/i.test(text));
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
function atris2TurnErrorReason(error) {
|
|
5842
|
+
return isTransientAtris2BackendError(error) ? 'atris2-backend-unavailable' : 'atris2-error';
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
function missionRunKeepsRetryingError(reason) {
|
|
5846
|
+
return reason === 'atris2-backend-unavailable';
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5564
5849
|
function isWithinActiveHours(activeHours, now = new Date()) {
|
|
5565
5850
|
if (!activeHours || !activeHours.start || !activeHours.end) return true;
|
|
5566
5851
|
const tz = activeHours.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
@@ -5617,6 +5902,11 @@ function releaseMissionLock(lock) {
|
|
|
5617
5902
|
|
|
5618
5903
|
function probeClaudeBinary() {
|
|
5619
5904
|
const runnerBin = resolveClaudeRunnerBin();
|
|
5905
|
+
if (resolveClaudeRunnerCommandTemplate()) {
|
|
5906
|
+
const probe = spawnSync('sh', ['-c', `command -v ${shellQuote(runnerBin)}`], { encoding: 'utf8', timeout: 8000 });
|
|
5907
|
+
if (probe.status !== 0) return { ok: false, error: `${runnerBin} CLI not found` };
|
|
5908
|
+
return { ok: true };
|
|
5909
|
+
}
|
|
5620
5910
|
const help = spawnSync(runnerBin, ['--help'], { encoding: 'utf8', timeout: 8000 });
|
|
5621
5911
|
if (help.status !== 0) return { ok: false, error: `${runnerBin} --help failed` };
|
|
5622
5912
|
const text = String(help.stdout || '');
|
|
@@ -5723,7 +6013,91 @@ function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastEr
|
|
|
5723
6013
|
return `resume with: atris mission run ${missionId}`;
|
|
5724
6014
|
}
|
|
5725
6015
|
|
|
6016
|
+
function writeRunnerPromptFile(cwd, missionId, prompt) {
|
|
6017
|
+
const dir = path.join(cwd, '.atris', 'state', 'runner-prompts');
|
|
6018
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
6019
|
+
const file = path.join(dir, `${missionId || 'mission'}-${Date.now()}-${crypto.randomUUID()}.md`);
|
|
6020
|
+
fs.writeFileSync(file, prompt, 'utf8');
|
|
6021
|
+
return file;
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
function spawnGenericRunnerTick(mission, opts) {
|
|
6025
|
+
const { sessionId, cwd, signal, timeoutMs, prompt, model } = opts;
|
|
6026
|
+
return new Promise((resolve) => {
|
|
6027
|
+
let promptFile = null;
|
|
6028
|
+
let cmd = '';
|
|
6029
|
+
try {
|
|
6030
|
+
promptFile = writeRunnerPromptFile(cwd, mission.id, prompt);
|
|
6031
|
+
cmd = buildRunnerCommand({ promptFile, model });
|
|
6032
|
+
} catch (e) {
|
|
6033
|
+
resolve({ ok: false, error: e.message, sessionIds: [], aborted: false, timedOut: false, authExpired: false });
|
|
6034
|
+
return;
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
const startedAt = Date.now();
|
|
6038
|
+
const proc = spawn('sh', ['-lc', cmd], { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
6039
|
+
let stdout = '';
|
|
6040
|
+
let stderr = '';
|
|
6041
|
+
let timedOut = false;
|
|
6042
|
+
let aborted = false;
|
|
6043
|
+
|
|
6044
|
+
const cleanupPrompt = () => {
|
|
6045
|
+
try { if (promptFile) fs.unlinkSync(promptFile); } catch {}
|
|
6046
|
+
};
|
|
6047
|
+
const kill = () => {
|
|
6048
|
+
const killGroup = (sig) => {
|
|
6049
|
+
try { process.kill(-proc.pid, sig); } catch { try { proc.kill(sig); } catch {} }
|
|
6050
|
+
};
|
|
6051
|
+
killGroup('SIGTERM');
|
|
6052
|
+
setTimeout(() => killGroup('SIGKILL'), 3000).unref();
|
|
6053
|
+
};
|
|
6054
|
+
const timer = setTimeout(() => { timedOut = true; kill(); }, timeoutMs);
|
|
6055
|
+
const onAbort = () => { aborted = true; kill(); };
|
|
6056
|
+
if (signal) {
|
|
6057
|
+
if (signal.aborted) onAbort();
|
|
6058
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
6062
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
6063
|
+
|
|
6064
|
+
proc.on('close', (code) => {
|
|
6065
|
+
clearTimeout(timer);
|
|
6066
|
+
cleanupPrompt();
|
|
6067
|
+
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
6068
|
+
const finalText = String(stdout || '').trim();
|
|
6069
|
+
const errStr = String(stderr || '').slice(-2000);
|
|
6070
|
+
const ok = code === 0 && !timedOut && !aborted;
|
|
6071
|
+
const authExpired = /not authenticated|please log in|login required|auth(?:entication)? expired/i.test(errStr);
|
|
6072
|
+
resolve({
|
|
6073
|
+
ok,
|
|
6074
|
+
timedOut,
|
|
6075
|
+
aborted,
|
|
6076
|
+
authExpired,
|
|
6077
|
+
exitCode: code,
|
|
6078
|
+
sessionIds: sessionId ? [sessionId] : [],
|
|
6079
|
+
result: finalText,
|
|
6080
|
+
summary: usefulClaudeReceiptSummary(finalText || errStr, ok ? 'no-text' : 'error'),
|
|
6081
|
+
receipt_text: cappedClaudeReceiptText(finalText || errStr),
|
|
6082
|
+
duration_total_ms: Date.now() - startedAt,
|
|
6083
|
+
num_turns: null,
|
|
6084
|
+
stop_reason: null,
|
|
6085
|
+
stderr: errStr,
|
|
6086
|
+
parse_errors: 0,
|
|
6087
|
+
});
|
|
6088
|
+
});
|
|
6089
|
+
|
|
6090
|
+
proc.on('error', (e) => {
|
|
6091
|
+
clearTimeout(timer);
|
|
6092
|
+
cleanupPrompt();
|
|
6093
|
+
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
6094
|
+
resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false });
|
|
6095
|
+
});
|
|
6096
|
+
});
|
|
6097
|
+
}
|
|
6098
|
+
|
|
5726
6099
|
function spawnClaudeTick(mission, opts) {
|
|
6100
|
+
if (resolveClaudeRunnerCommandTemplate()) return spawnGenericRunnerTick(mission, opts);
|
|
5727
6101
|
const { sessionMode, sessionId, cwd, signal, timeoutMs, prompt, model } = opts;
|
|
5728
6102
|
return new Promise((resolve) => {
|
|
5729
6103
|
const args = [
|
|
@@ -5874,10 +6248,14 @@ async function runMission(args) {
|
|
|
5874
6248
|
if (hasFlag(args, '--fleet')) {
|
|
5875
6249
|
const { runFleetFlight } = require('../lib/fleet');
|
|
5876
6250
|
const slots = Math.max(1, Number(readFlag(args, '--slots', '')) || 3);
|
|
6251
|
+
// Default: cut build worktrees from origin/master (checkoutBase). --base
|
|
6252
|
+
// keeps launcher-HEAD only when the operator asks for it explicitly.
|
|
6253
|
+
const baseOverride = readFlag(args, '--base', '');
|
|
5877
6254
|
const flight = await runFleetFlight({
|
|
5878
6255
|
slots,
|
|
5879
6256
|
dryRun: hasFlag(args, '--dry-run'),
|
|
5880
6257
|
log: asJson ? () => {} : console.log,
|
|
6258
|
+
...(baseOverride ? { checkoutBase: baseOverride } : {}),
|
|
5881
6259
|
});
|
|
5882
6260
|
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
5883
6261
|
process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
|
|
@@ -5894,6 +6272,12 @@ async function runMission(args) {
|
|
|
5894
6272
|
const maxWallFlag = readFlag(args, '--max-wall', '');
|
|
5895
6273
|
let maxWallSeconds = Math.max(60, Number(maxWallFlag) || MISSION_RUN_DEFAULTS.maxWallSeconds);
|
|
5896
6274
|
const cadenceOverride = readFlag(args, '--cadence', '');
|
|
6275
|
+
const engineOverrideRaw = readFlag(args, '--engine', '');
|
|
6276
|
+
const runnerOverride = engineOverrideRaw
|
|
6277
|
+
? resolveMissionRunnerSelection(engineOverrideRaw, { asJson, engineOnly: true, label: 'engine' })
|
|
6278
|
+
: null;
|
|
6279
|
+
const modelOverride = readFlag(args, '--model', '');
|
|
6280
|
+
const runtimeView = (baseMission) => missionRunRuntimeView(baseMission, runnerOverride, modelOverride);
|
|
5897
6281
|
const input = missionRunInputFromArgs(args);
|
|
5898
6282
|
const ref = input.ref;
|
|
5899
6283
|
const runArgs = input.args;
|
|
@@ -5921,7 +6305,8 @@ async function runMission(args) {
|
|
|
5921
6305
|
return;
|
|
5922
6306
|
}
|
|
5923
6307
|
if (!mission) {
|
|
5924
|
-
|
|
6308
|
+
if (ref) exitMissingMission(ref, 1, asJson);
|
|
6309
|
+
exitMissionError('Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
|
|
5925
6310
|
}
|
|
5926
6311
|
if (!maxWallFlag && Number(mission.budget_contract?.requested_seconds) > 0) {
|
|
5927
6312
|
maxWallSeconds = Math.max(60, Number(mission.budget_contract.requested_seconds));
|
|
@@ -5939,17 +6324,22 @@ async function runMission(args) {
|
|
|
5939
6324
|
process.exit(0);
|
|
5940
6325
|
}
|
|
5941
6326
|
|
|
5942
|
-
|
|
6327
|
+
const manualAck = hasFlag(args, '--manual-ack');
|
|
6328
|
+
const nativeGoalStatus = readFlag(args, '--native-goal-status', readFlag(args, '--visible-goal-status', ''));
|
|
6329
|
+
const nativeGoalObjective = readFlag(args, '--native-goal-objective', readFlag(args, '--visible-goal-objective', ''));
|
|
6330
|
+
const nativeGoalRunOptions = {
|
|
6331
|
+
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
6332
|
+
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
6333
|
+
...(manualAck ? { manualAck: true } : {}),
|
|
6334
|
+
};
|
|
6335
|
+
const autoAck = maybeAutoAckCodexNativeGoal(mission, process.cwd(), nativeGoalRunOptions);
|
|
6336
|
+
if (autoAck) mission = autoAck.saved;
|
|
6337
|
+
maybeBlockUntilCodexNativeGoalStarted(runtimeView(mission), asJson, { manualAck });
|
|
5943
6338
|
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
if (!probe.ok) {
|
|
5949
|
-
console.error(`[mission run] claude probe failed: ${probe.error}`);
|
|
5950
|
-
process.exit(2);
|
|
5951
|
-
}
|
|
5952
|
-
}
|
|
6339
|
+
// No pre-lock claude probe here: the runner profile isn't applied yet, so
|
|
6340
|
+
// probing would test the default claude bin even when the effective runner
|
|
6341
|
+
// (e.g. --engine cursor) never invokes claude. The in-lock probe below runs
|
|
6342
|
+
// after applyMissionRunnerProfile and checks the right binary.
|
|
5953
6343
|
|
|
5954
6344
|
const lock = acquireMissionLock(mission.id);
|
|
5955
6345
|
if (!lock.ok) {
|
|
@@ -5964,6 +6354,7 @@ async function runMission(args) {
|
|
|
5964
6354
|
let ranTicks = 0;
|
|
5965
6355
|
const ticks = [];
|
|
5966
6356
|
let onSig = null;
|
|
6357
|
+
let restoreRunnerProfile = null;
|
|
5967
6358
|
|
|
5968
6359
|
try {
|
|
5969
6360
|
const cwd = process.cwd();
|
|
@@ -5977,11 +6368,12 @@ async function runMission(args) {
|
|
|
5977
6368
|
// Derive sessionId, pendingSessionId, and the frozen contract from the fresh record
|
|
5978
6369
|
// so a fast tick's writes can't be silently overwritten by this run loop.
|
|
5979
6370
|
mission = resolveMission(mission.id) || mission;
|
|
6371
|
+
let runtimeMission = runtimeView(mission);
|
|
5980
6372
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
5981
6373
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
5982
6374
|
return;
|
|
5983
6375
|
}
|
|
5984
|
-
if (returnIfCodexNativeGoalNotStarted(
|
|
6376
|
+
if (returnIfCodexNativeGoalNotStarted(runtimeMission, asJson, { manualAck })) return;
|
|
5985
6377
|
if (mission.status === 'paused') {
|
|
5986
6378
|
mission = saveMission({
|
|
5987
6379
|
...mission,
|
|
@@ -5991,17 +6383,29 @@ async function runMission(args) {
|
|
|
5991
6383
|
stop_reason: null,
|
|
5992
6384
|
next_action: `running: atris mission run ${mission.id}`,
|
|
5993
6385
|
}, cwd, 'mission_run_resumed', { reason: 'operator-resume' }).mission;
|
|
6386
|
+
runtimeMission = runtimeView(mission);
|
|
5994
6387
|
}
|
|
5995
6388
|
sessionId = mission.claude_session_id || null;
|
|
5996
6389
|
pendingSessionId = mission.pending_session_id || null;
|
|
5997
|
-
|
|
5998
|
-
const
|
|
6390
|
+
restoreRunnerProfile = applyMissionRunnerProfile(runtimeMission.runner);
|
|
6391
|
+
const callerSessionRunner = runnerUsesCallerSession(runtimeMission.runner);
|
|
6392
|
+
const atris2Runner = String(runtimeMission.runner || '').trim().toLowerCase() === 'atris2';
|
|
5999
6393
|
const skipWorker = skipClaude || callerSessionRunner;
|
|
6394
|
+
if (!skipClaude && !callerSessionRunner && !atris2Runner) {
|
|
6395
|
+
const probe = probeClaudeBinary();
|
|
6396
|
+
if (!probe.ok) {
|
|
6397
|
+
console.error(`[mission run] claude probe failed: ${probe.error}`);
|
|
6398
|
+
process.exit(2);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6000
6401
|
|
|
6001
6402
|
// Freeze run-start contract (verifier, lane). Stored on receipts, not the mission record.
|
|
6002
6403
|
const frozen = {
|
|
6003
6404
|
verifier: effectiveMissionVerifier(mission),
|
|
6004
6405
|
lane: mission.lane || 'workspace',
|
|
6406
|
+
runner: runtimeMission.runner || 'manual',
|
|
6407
|
+
model: runtimeMission.model || null,
|
|
6408
|
+
...(runnerOverride ? { stored_runner: mission.runner || null, stored_model: mission.model || null } : {}),
|
|
6005
6409
|
started_at: stampIso(),
|
|
6006
6410
|
};
|
|
6007
6411
|
const runWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
@@ -6016,6 +6420,7 @@ async function runMission(args) {
|
|
|
6016
6420
|
if (!skipWorker && !atris2Runner && !sessionId && !pendingSessionId) {
|
|
6017
6421
|
pendingSessionId = crypto.randomUUID();
|
|
6018
6422
|
mission = saveMission({ ...mission, pending_session_id: pendingSessionId }, cwd, 'mission_session_pending', { session_id: pendingSessionId }).mission;
|
|
6423
|
+
runtimeMission = runtimeView(mission);
|
|
6019
6424
|
}
|
|
6020
6425
|
|
|
6021
6426
|
const startedAt = Date.now();
|
|
@@ -6026,7 +6431,7 @@ async function runMission(args) {
|
|
|
6026
6431
|
const sessionLabel = skipWorker
|
|
6027
6432
|
? 'caller-session'
|
|
6028
6433
|
: atris2Runner
|
|
6029
|
-
? `atris2 (${
|
|
6434
|
+
? `atris2 (${runtimeMission.model || 'atris:fast'})`
|
|
6030
6435
|
: (sessionId || `pending=${pendingSessionId}`);
|
|
6031
6436
|
if (!asJson) {
|
|
6032
6437
|
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}`);
|
|
@@ -6040,6 +6445,7 @@ async function runMission(args) {
|
|
|
6040
6445
|
|
|
6041
6446
|
// Re-read mission, detect mutation of frozen fields
|
|
6042
6447
|
mission = resolveMission(mission.id) || mission;
|
|
6448
|
+
runtimeMission = runtimeView(mission);
|
|
6043
6449
|
if (['complete', 'stopped', 'paused'].includes(mission.status)) { pauseReason = mission.status; break; }
|
|
6044
6450
|
if (effectiveMissionVerifier(mission) !== frozen.verifier) { pauseReason = 'verifier-mutated'; break; }
|
|
6045
6451
|
if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
|
|
@@ -6071,12 +6477,13 @@ async function runMission(args) {
|
|
|
6071
6477
|
} else if (atris2Runner) {
|
|
6072
6478
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
6073
6479
|
mission = pingDrain.mission;
|
|
6074
|
-
|
|
6480
|
+
runtimeMission = runtimeView(mission);
|
|
6481
|
+
const prompt = buildTickPrompt(runtimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
6075
6482
|
const { runAtris2Turn } = require('./probe');
|
|
6076
|
-
const businessId = businessIdForAtris2Mission(
|
|
6483
|
+
const businessId = businessIdForAtris2Mission(runtimeMission, cwd);
|
|
6077
6484
|
const turn = await runAtris2Turn({
|
|
6078
6485
|
prompt,
|
|
6079
|
-
model:
|
|
6486
|
+
model: runtimeMission.model || 'atris:fast',
|
|
6080
6487
|
business: businessId,
|
|
6081
6488
|
maxTurns: 16,
|
|
6082
6489
|
signal: controller.signal,
|
|
@@ -6084,17 +6491,18 @@ async function runMission(args) {
|
|
|
6084
6491
|
result.atris2 = {
|
|
6085
6492
|
ok: turn.ok,
|
|
6086
6493
|
engine: turn.engine,
|
|
6087
|
-
model:
|
|
6494
|
+
model: runtimeMission.model || 'atris:fast',
|
|
6088
6495
|
tools_run: turn.tools_run,
|
|
6089
6496
|
unsupported: turn.unsupported,
|
|
6090
6497
|
duration_ms: turn.duration_ms,
|
|
6091
6498
|
error: turn.error,
|
|
6499
|
+
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
6092
6500
|
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
6093
6501
|
};
|
|
6094
6502
|
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
6095
6503
|
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
6096
6504
|
if (!turn.ok || !String(turn.text || '').trim()) {
|
|
6097
|
-
result = { ...result, status: 'errored', reason:
|
|
6505
|
+
result = { ...result, status: 'errored', reason: atris2TurnErrorReason(turn.error) };
|
|
6098
6506
|
} else {
|
|
6099
6507
|
result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
|
|
6100
6508
|
}
|
|
@@ -6103,11 +6511,12 @@ async function runMission(args) {
|
|
|
6103
6511
|
const useId = sessionId || pendingSessionId;
|
|
6104
6512
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
6105
6513
|
mission = pingDrain.mission;
|
|
6106
|
-
|
|
6107
|
-
const
|
|
6514
|
+
runtimeMission = runtimeView(mission);
|
|
6515
|
+
const prompt = buildTickPrompt(runtimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
6516
|
+
const claudeResult = await spawnClaudeTick(runtimeMission, {
|
|
6108
6517
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
6109
6518
|
timeoutMs: MISSION_RUN_DEFAULTS.claudeTimeoutMs, prompt,
|
|
6110
|
-
model: resolveClaudeRunnerModel(
|
|
6519
|
+
model: resolveClaudeRunnerModel(runtimeMission),
|
|
6111
6520
|
});
|
|
6112
6521
|
result.claude = {
|
|
6113
6522
|
ok: claudeResult.ok,
|
|
@@ -6203,7 +6612,7 @@ async function runMission(args) {
|
|
|
6203
6612
|
const finishedAt = stampIso();
|
|
6204
6613
|
const tickRecord = { ...result, started_at: tickStart, finished_at: finishedAt, worktree: tickWorktree };
|
|
6205
6614
|
ticks.push(tickRecord);
|
|
6206
|
-
receiptPath = writeReceipt(
|
|
6615
|
+
receiptPath = writeReceipt(runtimeMission, {
|
|
6207
6616
|
kind: 'mission_run_tick',
|
|
6208
6617
|
tick: tickRecord,
|
|
6209
6618
|
frozen,
|
|
@@ -6315,7 +6724,9 @@ async function runMission(args) {
|
|
|
6315
6724
|
// is the same trap one step less deterministic: keep retrying and the loop burns every
|
|
6316
6725
|
// tick + cron firing on it. Halt at two-in-a-row and surface the reason for a human.
|
|
6317
6726
|
const errStreak = consecutiveSameReasonErrors(ticks);
|
|
6318
|
-
|
|
6727
|
+
// Sleeping Atris2 backends are different: leave the mission running so the
|
|
6728
|
+
// next tick or heartbeat can catch the backend after it wakes.
|
|
6729
|
+
if (errStreak.count >= 2 && !missionRunKeepsRetryingError(errStreak.reason)) { pauseReason = `repeated-error:${errStreak.reason}`; break; }
|
|
6319
6730
|
|
|
6320
6731
|
// Sleep until next tick
|
|
6321
6732
|
let sleepMs = 0;
|
|
@@ -6338,7 +6749,7 @@ async function runMission(args) {
|
|
|
6338
6749
|
|
|
6339
6750
|
if (!pauseReason && ticks.length >= effectiveMaxTicks) {
|
|
6340
6751
|
const lastTick = ticks[ticks.length - 1];
|
|
6341
|
-
if (lastTick && lastTick.status !== 'ran') pauseReason = 'max-ticks-reached';
|
|
6752
|
+
if (lastTick && lastTick.status !== 'ran' && !missionRunKeepsRetryingError(lastTick.reason)) pauseReason = 'max-ticks-reached';
|
|
6342
6753
|
}
|
|
6343
6754
|
|
|
6344
6755
|
if (pauseReason && !['complete', 'ready', 'max-wall-reached'].includes(pauseReason)) {
|
|
@@ -6366,7 +6777,8 @@ async function runMission(args) {
|
|
|
6366
6777
|
next: missionRunCreatedNextLine(createdNext, continuationGoal, mission),
|
|
6367
6778
|
};
|
|
6368
6779
|
landingSummary.reason = missionHumanReasonText(mission, landingSummary.changed);
|
|
6369
|
-
const
|
|
6780
|
+
const finalRuntimeMission = runtimeView(mission);
|
|
6781
|
+
const finalReceipt = writeReceipt(finalRuntimeMission, {
|
|
6370
6782
|
kind: 'mission_run_summary',
|
|
6371
6783
|
frozen,
|
|
6372
6784
|
pause_reason: pauseReason,
|
|
@@ -6385,7 +6797,7 @@ async function runMission(args) {
|
|
|
6385
6797
|
const codexGoalState = refreshCodexGoalController(cwd);
|
|
6386
6798
|
|
|
6387
6799
|
printJsonOrText(
|
|
6388
|
-
{ 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 },
|
|
6800
|
+
{ ok: true, action: 'mission_run', mission, runner_override: runnerOverride ? finalRuntimeMission.run_runner_override : null, 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 },
|
|
6389
6801
|
missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalReceipt, pauseReason, continuationGoal, ticks, createdNext),
|
|
6390
6802
|
asJson,
|
|
6391
6803
|
);
|
|
@@ -6394,12 +6806,20 @@ async function runMission(args) {
|
|
|
6394
6806
|
try { process.removeListener('SIGINT', onSig); } catch {}
|
|
6395
6807
|
try { process.removeListener('SIGTERM', onSig); } catch {}
|
|
6396
6808
|
}
|
|
6809
|
+
if (restoreRunnerProfile) {
|
|
6810
|
+
try { restoreRunnerProfile(); } catch {}
|
|
6811
|
+
}
|
|
6397
6812
|
releaseMissionLock(lock);
|
|
6398
6813
|
}
|
|
6399
6814
|
}
|
|
6400
6815
|
|
|
6401
6816
|
function tickMission(args) {
|
|
6402
6817
|
const asJson = wantsJson(args);
|
|
6818
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
6819
|
+
console.log('Usage: atris mission tick <id> [--verify] [--summary "..."] [--complete-on-pass]');
|
|
6820
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
6821
|
+
process.exit(0);
|
|
6822
|
+
}
|
|
6403
6823
|
const verify = hasFlag(args, '--verify');
|
|
6404
6824
|
const verifyOverride = readFlag(args, '--verify', '');
|
|
6405
6825
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
@@ -6735,6 +7155,60 @@ function stopMission(args) {
|
|
|
6735
7155
|
);
|
|
6736
7156
|
}
|
|
6737
7157
|
|
|
7158
|
+
// A mission parked in paused/planning/ready and untouched for a week is
|
|
7159
|
+
// abandoned in practice — nobody resumes it, and each one is a line the
|
|
7160
|
+
// operator re-reads forever. The daily autoland tick expires them to
|
|
7161
|
+
// stopped with a revive hint; running missions and anything touched
|
|
7162
|
+
// recently are never aged out, and a tick on an expired id revives it.
|
|
7163
|
+
const MISSION_IDLE_EXPIRY_DAYS = 7;
|
|
7164
|
+
const EXPIRABLE_STATUSES = new Set(['paused', 'planning', 'ready']);
|
|
7165
|
+
|
|
7166
|
+
function expireStaleMissions(root = process.cwd(), { idleDays = MISSION_IDLE_EXPIRY_DAYS, idleHours = null, dryRun = false, statuses = EXPIRABLE_STATUSES } = {}) {
|
|
7167
|
+
const windowMs = idleHours != null ? idleHours * 60 * 60 * 1000 : idleDays * 24 * 60 * 60 * 1000;
|
|
7168
|
+
const windowLabel = idleHours != null
|
|
7169
|
+
? `${idleHours}+ idle hour${idleHours === 1 ? '' : 's'}`
|
|
7170
|
+
: `${idleDays}+ idle day${idleDays === 1 ? '' : 's'}`;
|
|
7171
|
+
const cutoff = Date.now() - windowMs;
|
|
7172
|
+
const expired = [];
|
|
7173
|
+
for (const mission of listMissions(root)) {
|
|
7174
|
+
const status = String(mission.status || '').toLowerCase();
|
|
7175
|
+
if (!statuses.has(status)) continue;
|
|
7176
|
+
// updated_at and paused_at are machine-polluted — status renders and
|
|
7177
|
+
// goal controllers re-save parked missions daily, so a mission nobody
|
|
7178
|
+
// has run since May reads as "touched today". Real activity is the
|
|
7179
|
+
// last tick (or creation, for missions that never ran).
|
|
7180
|
+
const touched = Math.max(
|
|
7181
|
+
Date.parse(mission.last_tick_at || '') || 0,
|
|
7182
|
+
Date.parse(mission.created_at || '') || 0,
|
|
7183
|
+
);
|
|
7184
|
+
if (!touched || touched > cutoff) continue;
|
|
7185
|
+
const reason = `expired after ${windowLabel} (was ${status}); revive with: atris mission tick ${mission.id}`;
|
|
7186
|
+
if (!dryRun) {
|
|
7187
|
+
const next = {
|
|
7188
|
+
...mission,
|
|
7189
|
+
status: 'stopped',
|
|
7190
|
+
stopped_at: stampIso(),
|
|
7191
|
+
stop_reason: reason,
|
|
7192
|
+
};
|
|
7193
|
+
const { mission: saved } = saveMission(next, root, 'mission_expired', { reason, previous_status: status, idle_days: idleDays, idle_hours: idleHours });
|
|
7194
|
+
appendMemberLog(saved.owner, 'Mission expired', { mission: saved.objective, reason }, root);
|
|
7195
|
+
}
|
|
7196
|
+
expired.push({ id: mission.id, owner: mission.owner, previous_status: status, objective: String(mission.objective || '').slice(0, 120) });
|
|
7197
|
+
}
|
|
7198
|
+
return expired;
|
|
7199
|
+
}
|
|
7200
|
+
|
|
7201
|
+
// Zombie-mission reap: a mission left paused past a short leash (48h default)
|
|
7202
|
+
// is dead in practice long before the 7-day general idle expiry ever fires —
|
|
7203
|
+
// nobody is coming back to it inside a session, and it just sits on
|
|
7204
|
+
// `mission list` as noise. Narrower than expireStaleMissions on purpose: only
|
|
7205
|
+
// `paused`, never planning/ready, so the weekly cadence for those is unchanged.
|
|
7206
|
+
const MISSION_PAUSED_REAP_HOURS = 48;
|
|
7207
|
+
|
|
7208
|
+
function reapPausedMissions(root = process.cwd(), { hours = MISSION_PAUSED_REAP_HOURS, dryRun = false } = {}) {
|
|
7209
|
+
return expireStaleMissions(root, { idleHours: hours, dryRun, statuses: new Set(['paused']) });
|
|
7210
|
+
}
|
|
7211
|
+
|
|
6738
7212
|
function goalMission(args) {
|
|
6739
7213
|
const asJson = wantsJson(args);
|
|
6740
7214
|
if (args[0] === 'ack') {
|
|
@@ -6768,6 +7242,7 @@ function goalMission(args) {
|
|
|
6768
7242
|
heartbeat: heartbeatMode,
|
|
6769
7243
|
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
6770
7244
|
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
7245
|
+
...(hasFlag(args, '--manual-ack') ? { manualAck: true } : {}),
|
|
6771
7246
|
...(allowNativeGoalSupersede ? { allowNativeGoalSupersede: true } : {}),
|
|
6772
7247
|
});
|
|
6773
7248
|
if (payload.active_goal_conflict) {
|
|
@@ -6804,7 +7279,7 @@ function goalMission(args) {
|
|
|
6804
7279
|
|
|
6805
7280
|
function ackMissionGoal(args) {
|
|
6806
7281
|
const asJson = wantsJson(args);
|
|
6807
|
-
const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json'])[0] || '';
|
|
7282
|
+
const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json', '--manual-ack'])[0] || '';
|
|
6808
7283
|
if (!ref) {
|
|
6809
7284
|
exitMissionError('Usage: atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json', 1, asJson);
|
|
6810
7285
|
}
|
|
@@ -6814,7 +7289,28 @@ function ackMissionGoal(args) {
|
|
|
6814
7289
|
if (!mission) {
|
|
6815
7290
|
exitMissingMission(ref, 1, asJson);
|
|
6816
7291
|
}
|
|
6817
|
-
if (runtime !== 'codex'
|
|
7292
|
+
if (runtime !== 'codex') {
|
|
7293
|
+
const payload = {
|
|
7294
|
+
ok: true,
|
|
7295
|
+
action: 'native_goal_ack_skipped',
|
|
7296
|
+
mission: missionStatusView(mission),
|
|
7297
|
+
runtime,
|
|
7298
|
+
status,
|
|
7299
|
+
reason: 'runtime_not_codex',
|
|
7300
|
+
requires_native_goal_start: false,
|
|
7301
|
+
next_command: codexGoalNextCommand(mission),
|
|
7302
|
+
};
|
|
7303
|
+
printJsonOrText(
|
|
7304
|
+
payload,
|
|
7305
|
+
[
|
|
7306
|
+
`Native goal ack skipped for runtime=${runtime}.`,
|
|
7307
|
+
`Next: ${payload.next_command}`,
|
|
7308
|
+
],
|
|
7309
|
+
asJson,
|
|
7310
|
+
);
|
|
7311
|
+
return;
|
|
7312
|
+
}
|
|
7313
|
+
if (status !== 'active') {
|
|
6818
7314
|
exitMissionError('Native goal ack requires --runtime codex --status active.', 2, asJson);
|
|
6819
7315
|
}
|
|
6820
7316
|
|
|
@@ -6831,22 +7327,7 @@ function ackMissionGoal(args) {
|
|
|
6831
7327
|
}
|
|
6832
7328
|
const canonicalObjective = codexGoalObjective(mission);
|
|
6833
7329
|
const reportedObjective = readFlag(args, '--objective', canonicalObjective);
|
|
6834
|
-
const
|
|
6835
|
-
const ack = {
|
|
6836
|
-
runtime: 'codex',
|
|
6837
|
-
status: 'active',
|
|
6838
|
-
mission_id: mission.id,
|
|
6839
|
-
objective,
|
|
6840
|
-
...(reportedObjective && reportedObjective !== objective ? { reported_objective: reportedObjective } : {}),
|
|
6841
|
-
acknowledged_at: stampIso(),
|
|
6842
|
-
};
|
|
6843
|
-
const nextMission = {
|
|
6844
|
-
...mission,
|
|
6845
|
-
native_goal_ack: ack,
|
|
6846
|
-
next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
|
|
6847
|
-
};
|
|
6848
|
-
const supersededNativeGoalAcks = supersedeOtherCodexNativeGoalAcks(process.cwd(), nextMission, ack);
|
|
6849
|
-
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_native_goal_ack', { ack });
|
|
7330
|
+
const { saved, ack, supersededNativeGoalAcks } = recordCodexNativeGoalAck(mission, process.cwd(), { reportedObjective });
|
|
6850
7331
|
const payload = refreshCodexGoalController(process.cwd());
|
|
6851
7332
|
printJsonOrText(
|
|
6852
7333
|
{
|
|
@@ -6971,15 +7452,17 @@ atris mission - durable goal + loop + owner + proof state
|
|
|
6971
7452
|
(rolls up sibling git-worktree missions; --local scopes to this checkout)
|
|
6972
7453
|
atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json] Create a Mission Room card and shareable receipt from messy intent
|
|
6973
7454
|
atris mission prune-runs [--apply] [--days <n>] [--keep-newest <n>] [--json] Compress old run receipts into a manifest and prune unreferenced clutter
|
|
6974
|
-
atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--json]
|
|
7455
|
+
atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused] [--native-goal-objective "..."] [--manual-ack] [--allow-native-goal-supersede] [--json]
|
|
6975
7456
|
atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
|
|
6976
7457
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
6977
7458
|
atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
|
|
7459
|
+
atris mission set-runner <id> <runner|engine> [--model <id>] [--json]
|
|
6978
7460
|
atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
|
|
6979
7461
|
atris mission run --fleet [--slots 3] [--dry-run] [--json] Staff every idle capable engine on claimable safe-lane tasks: parallel worktree builds, serial rebase-before-ship landings, receipt in atris/runs/
|
|
6980
7462
|
atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
|
|
6981
|
-
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--take-goal-slot]
|
|
6982
|
-
[--
|
|
7463
|
+
[--native-goal-status active|paused] [--native-goal-objective "..."] [--manual-ack] [--allow-native-goal-supersede] [--take-goal-slot]
|
|
7464
|
+
[--engine <name>]
|
|
7465
|
+
[--spend-full-budget|--use-whole-budget|--stop-when-done] [--preflight|--no-preflight|--room-preflight|--no-room-preflight]
|
|
6983
7466
|
[--room-auto-run|--no-room-auto-run]
|
|
6984
7467
|
[--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
|
|
6985
7468
|
(bare/member-only run prompts; one-word fuzzy intent starts a new visible-goal mission; --due runs the saved queue)
|
|
@@ -7381,6 +7864,9 @@ function missionCommand(args) {
|
|
|
7381
7864
|
return inspectMission(rest);
|
|
7382
7865
|
case 'tick':
|
|
7383
7866
|
return tickMission(rest);
|
|
7867
|
+
case 'set-runner':
|
|
7868
|
+
case 'runner':
|
|
7869
|
+
return setMissionRunner(rest);
|
|
7384
7870
|
case 'run':
|
|
7385
7871
|
return runMission(rest);
|
|
7386
7872
|
case 'complete':
|
|
@@ -7402,6 +7888,8 @@ function missionCommand(args) {
|
|
|
7402
7888
|
module.exports = {
|
|
7403
7889
|
missionCommand,
|
|
7404
7890
|
inspectMission,
|
|
7891
|
+
expireStaleMissions,
|
|
7892
|
+
reapPausedMissions,
|
|
7405
7893
|
missionHeartbeatLines,
|
|
7406
7894
|
listMissions,
|
|
7407
7895
|
listWorktreeRollupMissions,
|
|
@@ -7411,6 +7899,7 @@ module.exports = {
|
|
|
7411
7899
|
selectDueMission,
|
|
7412
7900
|
selectAtrisGoalMission,
|
|
7413
7901
|
selectCodexGoalMission,
|
|
7902
|
+
codexGoalNextCommand,
|
|
7414
7903
|
usefulClaudeReceiptSummary,
|
|
7415
7904
|
cappedClaudeReceiptText,
|
|
7416
7905
|
extractLayerFromReceiptText,
|
|
@@ -7422,4 +7911,5 @@ module.exports = {
|
|
|
7422
7911
|
detectUnavailableModel,
|
|
7423
7912
|
missionPauseNextAction,
|
|
7424
7913
|
consecutiveSameReasonErrors,
|
|
7914
|
+
missionVerifierTimeoutMs,
|
|
7425
7915
|
};
|