atris 3.33.3 → 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 +19 -0
- package/ax +475 -17
- package/bin/atris.js +206 -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/loops.js +212 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +598 -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 +256 -13
- 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', '--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
|
}
|
|
@@ -2778,9 +2875,82 @@ function inheritedWorktreeBase(cwd) {
|
|
|
2778
2875
|
}
|
|
2779
2876
|
}
|
|
2780
2877
|
|
|
2878
|
+
// Dedup gate: the same objective + owner already active anywhere in the
|
|
2879
|
+
// workspace family (this store or any worktree's) is reused, never cloned.
|
|
2880
|
+
// Born 2026-07-02: an hourly alive loop spawned six identical auto-improver
|
|
2881
|
+
// missions in six fresh worktrees in one day — pure token burn. --duplicate
|
|
2882
|
+
// is the explicit escape hatch.
|
|
2883
|
+
const TWIN_ACTIVE_STATUSES = new Set(['planning', 'ready', 'running']);
|
|
2884
|
+
|
|
2885
|
+
function normalizedObjective(text) {
|
|
2886
|
+
return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
function findActiveTwinMission(objective, owner, root = process.cwd()) {
|
|
2890
|
+
const wantObjective = normalizedObjective(objective);
|
|
2891
|
+
const wantOwner = String(owner || '').trim().toLowerCase();
|
|
2892
|
+
if (!wantObjective) return null;
|
|
2893
|
+
const candidates = [
|
|
2894
|
+
...listMissions(root),
|
|
2895
|
+
...listWorktreeRollupMissions(root),
|
|
2896
|
+
];
|
|
2897
|
+
for (const m of candidates) {
|
|
2898
|
+
if (!m || !TWIN_ACTIVE_STATUSES.has(m.status)) continue;
|
|
2899
|
+
if (normalizedObjective(m.objective) !== wantObjective) continue;
|
|
2900
|
+
if (String(m.owner || '').trim().toLowerCase() !== wantOwner) continue;
|
|
2901
|
+
return m;
|
|
2902
|
+
}
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2781
2906
|
function startMission(args) {
|
|
2782
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
|
+
}
|
|
2783
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
|
+
}
|
|
2938
|
+
if (!hasFlag(args, '--duplicate')) {
|
|
2939
|
+
const twin = findActiveTwinMission(mission.objective, mission.owner);
|
|
2940
|
+
if (twin) {
|
|
2941
|
+
printJsonOrText(
|
|
2942
|
+
{ ok: true, action: 'mission_reused', reused: true, mission: twin, note: 'an active mission with this objective and owner already exists; resumed instead of cloning (pass --duplicate to force a second one)' },
|
|
2943
|
+
[
|
|
2944
|
+
`Already active: ${twin.id} (${twin.status})`,
|
|
2945
|
+
`Same objective, same owner — reusing it instead of starting a clone.`,
|
|
2946
|
+
`Resume: atris mission run ${twin.id}`,
|
|
2947
|
+
`Really want a second one: re-run with --duplicate`,
|
|
2948
|
+
],
|
|
2949
|
+
asJson,
|
|
2950
|
+
);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2784
2954
|
// --worktree: bind the mission to its own isolated checkout. We chdir before
|
|
2785
2955
|
// any state writes so the mission record, baseline sidecar, receipts, and
|
|
2786
2956
|
// member files all land inside the worktree — ticks run there, and the main
|
|
@@ -2805,7 +2975,7 @@ function startMission(args) {
|
|
|
2805
2975
|
mission.next_action = `work task then run: atris task current-step --goal-id ${mission.id} --as ${mission.owner} --proof "<proof>" --json`;
|
|
2806
2976
|
}
|
|
2807
2977
|
}
|
|
2808
|
-
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
2978
|
+
const warnings = [missingVerifierWarning(mission), missingOwnerMemberWarning(mission.owner)].filter(Boolean);
|
|
2809
2979
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
2810
2980
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective });
|
|
2811
2981
|
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
@@ -2915,7 +3085,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2915
3085
|
mission.xp_task = xpTask;
|
|
2916
3086
|
mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
|
|
2917
3087
|
}
|
|
2918
|
-
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
3088
|
+
const warnings = [missingVerifierWarning(mission), missingOwnerMemberWarning(mission.owner)].filter(Boolean);
|
|
2919
3089
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
2920
3090
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
|
|
2921
3091
|
const directGoalRequest = writeDirectRunCodexGoalRequest(saved, process.cwd());
|
|
@@ -2934,6 +3104,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2934
3104
|
const nativeGoalOptions = {
|
|
2935
3105
|
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
2936
3106
|
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
3107
|
+
...(hasFlag(args, '--manual-ack') ? { manualAck: true } : {}),
|
|
2937
3108
|
...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
|
|
2938
3109
|
};
|
|
2939
3110
|
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
@@ -2943,6 +3114,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2943
3114
|
const codexGoalState = runnerUsesCallerSession(saved.runner)
|
|
2944
3115
|
? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
|
|
2945
3116
|
: null;
|
|
3117
|
+
const outputMission = resolveMission(saved.id, process.cwd()) || saved;
|
|
2946
3118
|
const nativeGoal = codexGoalState?.native_goal_action
|
|
2947
3119
|
|| (codexGoalState?.goal?.requires_native_goal_start ? codexGoalState.goal.native_goal_action : null);
|
|
2948
3120
|
const nextCommand = codexGoalState?.next_command || codexGoalState?.goal?.next_command || atrisGoalState.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`;
|
|
@@ -2950,7 +3122,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2950
3122
|
{
|
|
2951
3123
|
ok: true,
|
|
2952
3124
|
action: 'mission_run_started',
|
|
2953
|
-
mission:
|
|
3125
|
+
mission: outputMission,
|
|
2954
3126
|
budget_contract: saved.budget_contract || null,
|
|
2955
3127
|
warnings,
|
|
2956
3128
|
state_path: statePaths().missionsJsonl,
|
|
@@ -2972,7 +3144,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2972
3144
|
native_goal_ack_command: codexGoalState?.goal?.native_goal_ack_command || codexGoalState?.active_goal_conflict?.commands?.ack_new_mission || null,
|
|
2973
3145
|
next_command: nextCommand,
|
|
2974
3146
|
},
|
|
2975
|
-
missionRunTakeoffLines(
|
|
3147
|
+
missionRunTakeoffLines(outputMission, { warnings, nextCommand }),
|
|
2976
3148
|
asJson,
|
|
2977
3149
|
);
|
|
2978
3150
|
}
|
|
@@ -3107,6 +3279,74 @@ function attachMissionTask(args) {
|
|
|
3107
3279
|
}
|
|
3108
3280
|
}
|
|
3109
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
|
+
|
|
3110
3350
|
function statusMission(args) {
|
|
3111
3351
|
const asJson = wantsJson(args);
|
|
3112
3352
|
const localOnly = hasFlag(args, '--local');
|
|
@@ -3986,6 +4226,12 @@ function missionReceiptHealth(mission, root = process.cwd()) {
|
|
|
3986
4226
|
return { ok: true, reason: 'verifier_passed', receipt_path: receiptPath };
|
|
3987
4227
|
}
|
|
3988
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
|
+
|
|
3989
4235
|
function collectMissionDoctorFindings(root = process.cwd(), options = {}) {
|
|
3990
4236
|
const localOnly = options.localOnly === true;
|
|
3991
4237
|
let missions = listMissions(root);
|
|
@@ -4017,7 +4263,10 @@ function collectMissionDoctorFindings(root = process.cwd(), options = {}) {
|
|
|
4017
4263
|
const status = String(mission.status || '');
|
|
4018
4264
|
const active = !TERMINAL_STATUSES.has(status);
|
|
4019
4265
|
const objective = String(mission.objective || '').trim();
|
|
4020
|
-
|
|
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)) {
|
|
4021
4270
|
add(
|
|
4022
4271
|
mission,
|
|
4023
4272
|
'missing_verifier',
|
|
@@ -4187,6 +4436,12 @@ function resolveVerifierCommand(command) {
|
|
|
4187
4436
|
return `${leading}${shellQuote(process.execPath)} ${shellQuote(cliPath)}${trimmed.slice('atris'.length)}`;
|
|
4188
4437
|
}
|
|
4189
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
|
+
|
|
4190
4445
|
function runVerifier(command, root = process.cwd()) {
|
|
4191
4446
|
if (!command) return null;
|
|
4192
4447
|
const resolvedCommand = resolveVerifierCommand(command);
|
|
@@ -4194,7 +4449,7 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
4194
4449
|
cwd: root,
|
|
4195
4450
|
shell: true,
|
|
4196
4451
|
encoding: 'utf8',
|
|
4197
|
-
timeout:
|
|
4452
|
+
timeout: missionVerifierTimeoutMs(),
|
|
4198
4453
|
env: process.env,
|
|
4199
4454
|
});
|
|
4200
4455
|
return {
|
|
@@ -4692,10 +4947,55 @@ function codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission) {
|
|
|
4692
4947
|
return runtimeGoalState.objective === codexGoalObjective(mission);
|
|
4693
4948
|
}
|
|
4694
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
|
+
|
|
4695
4957
|
function codexGoalAckCommand(mission, objective = codexGoalObjective(mission)) {
|
|
4696
4958
|
return `atris mission goal ack ${mission.id} --runtime codex --status active --objective ${shellQuote(objective)} --json`;
|
|
4697
4959
|
}
|
|
4698
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
|
+
|
|
4699
4999
|
function codexNativeGoalReplaceAction(newMission, activeMission, runtimeGoalState = null, commands = {}) {
|
|
4700
5000
|
const fromObjective = codexGoalObjective(activeMission);
|
|
4701
5001
|
const toObjective = codexGoalObjective(newMission);
|
|
@@ -4966,16 +5266,18 @@ function codexNativeGoalBlockPayload(mission) {
|
|
|
4966
5266
|
};
|
|
4967
5267
|
}
|
|
4968
5268
|
|
|
4969
|
-
function maybeBlockUntilCodexNativeGoalStarted(mission, asJson) {
|
|
5269
|
+
function maybeBlockUntilCodexNativeGoalStarted(mission, asJson, options = {}) {
|
|
4970
5270
|
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return;
|
|
5271
|
+
if (options.manualAck === false) return;
|
|
4971
5272
|
const payload = codexNativeGoalBlockPayload(mission);
|
|
4972
5273
|
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
4973
5274
|
else console.error(payload.next_action);
|
|
4974
5275
|
process.exit(2);
|
|
4975
5276
|
}
|
|
4976
5277
|
|
|
4977
|
-
function returnIfCodexNativeGoalNotStarted(mission, asJson) {
|
|
5278
|
+
function returnIfCodexNativeGoalNotStarted(mission, asJson, options = {}) {
|
|
4978
5279
|
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return false;
|
|
5280
|
+
if (options.manualAck === false) return false;
|
|
4979
5281
|
const payload = codexNativeGoalBlockPayload(mission);
|
|
4980
5282
|
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
4981
5283
|
else console.error(payload.next_action);
|
|
@@ -5162,7 +5464,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5162
5464
|
};
|
|
5163
5465
|
}
|
|
5164
5466
|
|
|
5165
|
-
|
|
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;
|
|
5166
5471
|
const taskSpine = missionTaskSpine(mission);
|
|
5167
5472
|
const missionView = missionStatusView(mission);
|
|
5168
5473
|
const objective = codexGoalObjective(mission);
|
|
@@ -5198,6 +5503,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5198
5503
|
native_goal_action: nativeGoalAction,
|
|
5199
5504
|
native_goal_ack_command: codexGoalAckCommand(mission, objective),
|
|
5200
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,
|
|
5201
5510
|
direct_goal_request: directGoalRequest || null,
|
|
5202
5511
|
seeded_continuation_goal: seededContinuationGoal || null,
|
|
5203
5512
|
next_action_preview: missionChoosesNextMission(mission) ? chooseNextMissionPreview(mission, root) : (mission.next_action_preview || null),
|
|
@@ -5213,6 +5522,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5213
5522
|
requires_native_goal_start: goal.requires_native_goal_start,
|
|
5214
5523
|
requires_native_goal_replace: goal.requires_native_goal_replace,
|
|
5215
5524
|
native_goal_action: goal.native_goal_action,
|
|
5525
|
+
auto_native_goal_ack: goal.auto_native_goal_ack,
|
|
5216
5526
|
runtime_goal_state: runtimeGoalState,
|
|
5217
5527
|
};
|
|
5218
5528
|
}
|
|
@@ -5517,6 +5827,25 @@ function consecutiveSameReasonErrors(ticks) {
|
|
|
5517
5827
|
return { reason: last.reason, count };
|
|
5518
5828
|
}
|
|
5519
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
|
+
|
|
5520
5849
|
function isWithinActiveHours(activeHours, now = new Date()) {
|
|
5521
5850
|
if (!activeHours || !activeHours.start || !activeHours.end) return true;
|
|
5522
5851
|
const tz = activeHours.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
@@ -5573,6 +5902,11 @@ function releaseMissionLock(lock) {
|
|
|
5573
5902
|
|
|
5574
5903
|
function probeClaudeBinary() {
|
|
5575
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
|
+
}
|
|
5576
5910
|
const help = spawnSync(runnerBin, ['--help'], { encoding: 'utf8', timeout: 8000 });
|
|
5577
5911
|
if (help.status !== 0) return { ok: false, error: `${runnerBin} --help failed` };
|
|
5578
5912
|
const text = String(help.stdout || '');
|
|
@@ -5679,7 +6013,91 @@ function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastEr
|
|
|
5679
6013
|
return `resume with: atris mission run ${missionId}`;
|
|
5680
6014
|
}
|
|
5681
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
|
+
|
|
5682
6099
|
function spawnClaudeTick(mission, opts) {
|
|
6100
|
+
if (resolveClaudeRunnerCommandTemplate()) return spawnGenericRunnerTick(mission, opts);
|
|
5683
6101
|
const { sessionMode, sessionId, cwd, signal, timeoutMs, prompt, model } = opts;
|
|
5684
6102
|
return new Promise((resolve) => {
|
|
5685
6103
|
const args = [
|
|
@@ -5830,10 +6248,14 @@ async function runMission(args) {
|
|
|
5830
6248
|
if (hasFlag(args, '--fleet')) {
|
|
5831
6249
|
const { runFleetFlight } = require('../lib/fleet');
|
|
5832
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', '');
|
|
5833
6254
|
const flight = await runFleetFlight({
|
|
5834
6255
|
slots,
|
|
5835
6256
|
dryRun: hasFlag(args, '--dry-run'),
|
|
5836
6257
|
log: asJson ? () => {} : console.log,
|
|
6258
|
+
...(baseOverride ? { checkoutBase: baseOverride } : {}),
|
|
5837
6259
|
});
|
|
5838
6260
|
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
5839
6261
|
process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
|
|
@@ -5850,6 +6272,12 @@ async function runMission(args) {
|
|
|
5850
6272
|
const maxWallFlag = readFlag(args, '--max-wall', '');
|
|
5851
6273
|
let maxWallSeconds = Math.max(60, Number(maxWallFlag) || MISSION_RUN_DEFAULTS.maxWallSeconds);
|
|
5852
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);
|
|
5853
6281
|
const input = missionRunInputFromArgs(args);
|
|
5854
6282
|
const ref = input.ref;
|
|
5855
6283
|
const runArgs = input.args;
|
|
@@ -5877,7 +6305,8 @@ async function runMission(args) {
|
|
|
5877
6305
|
return;
|
|
5878
6306
|
}
|
|
5879
6307
|
if (!mission) {
|
|
5880
|
-
|
|
6308
|
+
if (ref) exitMissingMission(ref, 1, asJson);
|
|
6309
|
+
exitMissionError('Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
|
|
5881
6310
|
}
|
|
5882
6311
|
if (!maxWallFlag && Number(mission.budget_contract?.requested_seconds) > 0) {
|
|
5883
6312
|
maxWallSeconds = Math.max(60, Number(mission.budget_contract.requested_seconds));
|
|
@@ -5895,17 +6324,22 @@ async function runMission(args) {
|
|
|
5895
6324
|
process.exit(0);
|
|
5896
6325
|
}
|
|
5897
6326
|
|
|
5898
|
-
|
|
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 });
|
|
5899
6338
|
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
if (!probe.ok) {
|
|
5905
|
-
console.error(`[mission run] claude probe failed: ${probe.error}`);
|
|
5906
|
-
process.exit(2);
|
|
5907
|
-
}
|
|
5908
|
-
}
|
|
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.
|
|
5909
6343
|
|
|
5910
6344
|
const lock = acquireMissionLock(mission.id);
|
|
5911
6345
|
if (!lock.ok) {
|
|
@@ -5920,6 +6354,7 @@ async function runMission(args) {
|
|
|
5920
6354
|
let ranTicks = 0;
|
|
5921
6355
|
const ticks = [];
|
|
5922
6356
|
let onSig = null;
|
|
6357
|
+
let restoreRunnerProfile = null;
|
|
5923
6358
|
|
|
5924
6359
|
try {
|
|
5925
6360
|
const cwd = process.cwd();
|
|
@@ -5933,11 +6368,12 @@ async function runMission(args) {
|
|
|
5933
6368
|
// Derive sessionId, pendingSessionId, and the frozen contract from the fresh record
|
|
5934
6369
|
// so a fast tick's writes can't be silently overwritten by this run loop.
|
|
5935
6370
|
mission = resolveMission(mission.id) || mission;
|
|
6371
|
+
let runtimeMission = runtimeView(mission);
|
|
5936
6372
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
5937
6373
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
5938
6374
|
return;
|
|
5939
6375
|
}
|
|
5940
|
-
if (returnIfCodexNativeGoalNotStarted(
|
|
6376
|
+
if (returnIfCodexNativeGoalNotStarted(runtimeMission, asJson, { manualAck })) return;
|
|
5941
6377
|
if (mission.status === 'paused') {
|
|
5942
6378
|
mission = saveMission({
|
|
5943
6379
|
...mission,
|
|
@@ -5947,17 +6383,29 @@ async function runMission(args) {
|
|
|
5947
6383
|
stop_reason: null,
|
|
5948
6384
|
next_action: `running: atris mission run ${mission.id}`,
|
|
5949
6385
|
}, cwd, 'mission_run_resumed', { reason: 'operator-resume' }).mission;
|
|
6386
|
+
runtimeMission = runtimeView(mission);
|
|
5950
6387
|
}
|
|
5951
6388
|
sessionId = mission.claude_session_id || null;
|
|
5952
6389
|
pendingSessionId = mission.pending_session_id || null;
|
|
5953
|
-
|
|
5954
|
-
const
|
|
6390
|
+
restoreRunnerProfile = applyMissionRunnerProfile(runtimeMission.runner);
|
|
6391
|
+
const callerSessionRunner = runnerUsesCallerSession(runtimeMission.runner);
|
|
6392
|
+
const atris2Runner = String(runtimeMission.runner || '').trim().toLowerCase() === 'atris2';
|
|
5955
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
|
+
}
|
|
5956
6401
|
|
|
5957
6402
|
// Freeze run-start contract (verifier, lane). Stored on receipts, not the mission record.
|
|
5958
6403
|
const frozen = {
|
|
5959
6404
|
verifier: effectiveMissionVerifier(mission),
|
|
5960
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 } : {}),
|
|
5961
6409
|
started_at: stampIso(),
|
|
5962
6410
|
};
|
|
5963
6411
|
const runWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
@@ -5972,6 +6420,7 @@ async function runMission(args) {
|
|
|
5972
6420
|
if (!skipWorker && !atris2Runner && !sessionId && !pendingSessionId) {
|
|
5973
6421
|
pendingSessionId = crypto.randomUUID();
|
|
5974
6422
|
mission = saveMission({ ...mission, pending_session_id: pendingSessionId }, cwd, 'mission_session_pending', { session_id: pendingSessionId }).mission;
|
|
6423
|
+
runtimeMission = runtimeView(mission);
|
|
5975
6424
|
}
|
|
5976
6425
|
|
|
5977
6426
|
const startedAt = Date.now();
|
|
@@ -5982,7 +6431,7 @@ async function runMission(args) {
|
|
|
5982
6431
|
const sessionLabel = skipWorker
|
|
5983
6432
|
? 'caller-session'
|
|
5984
6433
|
: atris2Runner
|
|
5985
|
-
? `atris2 (${
|
|
6434
|
+
? `atris2 (${runtimeMission.model || 'atris:fast'})`
|
|
5986
6435
|
: (sessionId || `pending=${pendingSessionId}`);
|
|
5987
6436
|
if (!asJson) {
|
|
5988
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}`);
|
|
@@ -5996,6 +6445,7 @@ async function runMission(args) {
|
|
|
5996
6445
|
|
|
5997
6446
|
// Re-read mission, detect mutation of frozen fields
|
|
5998
6447
|
mission = resolveMission(mission.id) || mission;
|
|
6448
|
+
runtimeMission = runtimeView(mission);
|
|
5999
6449
|
if (['complete', 'stopped', 'paused'].includes(mission.status)) { pauseReason = mission.status; break; }
|
|
6000
6450
|
if (effectiveMissionVerifier(mission) !== frozen.verifier) { pauseReason = 'verifier-mutated'; break; }
|
|
6001
6451
|
if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
|
|
@@ -6027,12 +6477,13 @@ async function runMission(args) {
|
|
|
6027
6477
|
} else if (atris2Runner) {
|
|
6028
6478
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
6029
6479
|
mission = pingDrain.mission;
|
|
6030
|
-
|
|
6480
|
+
runtimeMission = runtimeView(mission);
|
|
6481
|
+
const prompt = buildTickPrompt(runtimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
6031
6482
|
const { runAtris2Turn } = require('./probe');
|
|
6032
|
-
const businessId = businessIdForAtris2Mission(
|
|
6483
|
+
const businessId = businessIdForAtris2Mission(runtimeMission, cwd);
|
|
6033
6484
|
const turn = await runAtris2Turn({
|
|
6034
6485
|
prompt,
|
|
6035
|
-
model:
|
|
6486
|
+
model: runtimeMission.model || 'atris:fast',
|
|
6036
6487
|
business: businessId,
|
|
6037
6488
|
maxTurns: 16,
|
|
6038
6489
|
signal: controller.signal,
|
|
@@ -6040,17 +6491,18 @@ async function runMission(args) {
|
|
|
6040
6491
|
result.atris2 = {
|
|
6041
6492
|
ok: turn.ok,
|
|
6042
6493
|
engine: turn.engine,
|
|
6043
|
-
model:
|
|
6494
|
+
model: runtimeMission.model || 'atris:fast',
|
|
6044
6495
|
tools_run: turn.tools_run,
|
|
6045
6496
|
unsupported: turn.unsupported,
|
|
6046
6497
|
duration_ms: turn.duration_ms,
|
|
6047
6498
|
error: turn.error,
|
|
6499
|
+
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
6048
6500
|
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
6049
6501
|
};
|
|
6050
6502
|
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
6051
6503
|
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
6052
6504
|
if (!turn.ok || !String(turn.text || '').trim()) {
|
|
6053
|
-
result = { ...result, status: 'errored', reason:
|
|
6505
|
+
result = { ...result, status: 'errored', reason: atris2TurnErrorReason(turn.error) };
|
|
6054
6506
|
} else {
|
|
6055
6507
|
result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
|
|
6056
6508
|
}
|
|
@@ -6059,11 +6511,12 @@ async function runMission(args) {
|
|
|
6059
6511
|
const useId = sessionId || pendingSessionId;
|
|
6060
6512
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
6061
6513
|
mission = pingDrain.mission;
|
|
6062
|
-
|
|
6063
|
-
const
|
|
6514
|
+
runtimeMission = runtimeView(mission);
|
|
6515
|
+
const prompt = buildTickPrompt(runtimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
6516
|
+
const claudeResult = await spawnClaudeTick(runtimeMission, {
|
|
6064
6517
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
6065
6518
|
timeoutMs: MISSION_RUN_DEFAULTS.claudeTimeoutMs, prompt,
|
|
6066
|
-
model: resolveClaudeRunnerModel(
|
|
6519
|
+
model: resolveClaudeRunnerModel(runtimeMission),
|
|
6067
6520
|
});
|
|
6068
6521
|
result.claude = {
|
|
6069
6522
|
ok: claudeResult.ok,
|
|
@@ -6159,7 +6612,7 @@ async function runMission(args) {
|
|
|
6159
6612
|
const finishedAt = stampIso();
|
|
6160
6613
|
const tickRecord = { ...result, started_at: tickStart, finished_at: finishedAt, worktree: tickWorktree };
|
|
6161
6614
|
ticks.push(tickRecord);
|
|
6162
|
-
receiptPath = writeReceipt(
|
|
6615
|
+
receiptPath = writeReceipt(runtimeMission, {
|
|
6163
6616
|
kind: 'mission_run_tick',
|
|
6164
6617
|
tick: tickRecord,
|
|
6165
6618
|
frozen,
|
|
@@ -6271,7 +6724,9 @@ async function runMission(args) {
|
|
|
6271
6724
|
// is the same trap one step less deterministic: keep retrying and the loop burns every
|
|
6272
6725
|
// tick + cron firing on it. Halt at two-in-a-row and surface the reason for a human.
|
|
6273
6726
|
const errStreak = consecutiveSameReasonErrors(ticks);
|
|
6274
|
-
|
|
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; }
|
|
6275
6730
|
|
|
6276
6731
|
// Sleep until next tick
|
|
6277
6732
|
let sleepMs = 0;
|
|
@@ -6294,7 +6749,7 @@ async function runMission(args) {
|
|
|
6294
6749
|
|
|
6295
6750
|
if (!pauseReason && ticks.length >= effectiveMaxTicks) {
|
|
6296
6751
|
const lastTick = ticks[ticks.length - 1];
|
|
6297
|
-
if (lastTick && lastTick.status !== 'ran') pauseReason = 'max-ticks-reached';
|
|
6752
|
+
if (lastTick && lastTick.status !== 'ran' && !missionRunKeepsRetryingError(lastTick.reason)) pauseReason = 'max-ticks-reached';
|
|
6298
6753
|
}
|
|
6299
6754
|
|
|
6300
6755
|
if (pauseReason && !['complete', 'ready', 'max-wall-reached'].includes(pauseReason)) {
|
|
@@ -6322,7 +6777,8 @@ async function runMission(args) {
|
|
|
6322
6777
|
next: missionRunCreatedNextLine(createdNext, continuationGoal, mission),
|
|
6323
6778
|
};
|
|
6324
6779
|
landingSummary.reason = missionHumanReasonText(mission, landingSummary.changed);
|
|
6325
|
-
const
|
|
6780
|
+
const finalRuntimeMission = runtimeView(mission);
|
|
6781
|
+
const finalReceipt = writeReceipt(finalRuntimeMission, {
|
|
6326
6782
|
kind: 'mission_run_summary',
|
|
6327
6783
|
frozen,
|
|
6328
6784
|
pause_reason: pauseReason,
|
|
@@ -6341,7 +6797,7 @@ async function runMission(args) {
|
|
|
6341
6797
|
const codexGoalState = refreshCodexGoalController(cwd);
|
|
6342
6798
|
|
|
6343
6799
|
printJsonOrText(
|
|
6344
|
-
{ 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 },
|
|
6345
6801
|
missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalReceipt, pauseReason, continuationGoal, ticks, createdNext),
|
|
6346
6802
|
asJson,
|
|
6347
6803
|
);
|
|
@@ -6350,12 +6806,20 @@ async function runMission(args) {
|
|
|
6350
6806
|
try { process.removeListener('SIGINT', onSig); } catch {}
|
|
6351
6807
|
try { process.removeListener('SIGTERM', onSig); } catch {}
|
|
6352
6808
|
}
|
|
6809
|
+
if (restoreRunnerProfile) {
|
|
6810
|
+
try { restoreRunnerProfile(); } catch {}
|
|
6811
|
+
}
|
|
6353
6812
|
releaseMissionLock(lock);
|
|
6354
6813
|
}
|
|
6355
6814
|
}
|
|
6356
6815
|
|
|
6357
6816
|
function tickMission(args) {
|
|
6358
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
|
+
}
|
|
6359
6823
|
const verify = hasFlag(args, '--verify');
|
|
6360
6824
|
const verifyOverride = readFlag(args, '--verify', '');
|
|
6361
6825
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
@@ -6691,6 +7155,60 @@ function stopMission(args) {
|
|
|
6691
7155
|
);
|
|
6692
7156
|
}
|
|
6693
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
|
+
|
|
6694
7212
|
function goalMission(args) {
|
|
6695
7213
|
const asJson = wantsJson(args);
|
|
6696
7214
|
if (args[0] === 'ack') {
|
|
@@ -6724,6 +7242,7 @@ function goalMission(args) {
|
|
|
6724
7242
|
heartbeat: heartbeatMode,
|
|
6725
7243
|
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
6726
7244
|
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
7245
|
+
...(hasFlag(args, '--manual-ack') ? { manualAck: true } : {}),
|
|
6727
7246
|
...(allowNativeGoalSupersede ? { allowNativeGoalSupersede: true } : {}),
|
|
6728
7247
|
});
|
|
6729
7248
|
if (payload.active_goal_conflict) {
|
|
@@ -6760,7 +7279,7 @@ function goalMission(args) {
|
|
|
6760
7279
|
|
|
6761
7280
|
function ackMissionGoal(args) {
|
|
6762
7281
|
const asJson = wantsJson(args);
|
|
6763
|
-
const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json'])[0] || '';
|
|
7282
|
+
const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json', '--manual-ack'])[0] || '';
|
|
6764
7283
|
if (!ref) {
|
|
6765
7284
|
exitMissionError('Usage: atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json', 1, asJson);
|
|
6766
7285
|
}
|
|
@@ -6770,7 +7289,28 @@ function ackMissionGoal(args) {
|
|
|
6770
7289
|
if (!mission) {
|
|
6771
7290
|
exitMissingMission(ref, 1, asJson);
|
|
6772
7291
|
}
|
|
6773
|
-
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') {
|
|
6774
7314
|
exitMissionError('Native goal ack requires --runtime codex --status active.', 2, asJson);
|
|
6775
7315
|
}
|
|
6776
7316
|
|
|
@@ -6787,22 +7327,7 @@ function ackMissionGoal(args) {
|
|
|
6787
7327
|
}
|
|
6788
7328
|
const canonicalObjective = codexGoalObjective(mission);
|
|
6789
7329
|
const reportedObjective = readFlag(args, '--objective', canonicalObjective);
|
|
6790
|
-
const
|
|
6791
|
-
const ack = {
|
|
6792
|
-
runtime: 'codex',
|
|
6793
|
-
status: 'active',
|
|
6794
|
-
mission_id: mission.id,
|
|
6795
|
-
objective,
|
|
6796
|
-
...(reportedObjective && reportedObjective !== objective ? { reported_objective: reportedObjective } : {}),
|
|
6797
|
-
acknowledged_at: stampIso(),
|
|
6798
|
-
};
|
|
6799
|
-
const nextMission = {
|
|
6800
|
-
...mission,
|
|
6801
|
-
native_goal_ack: ack,
|
|
6802
|
-
next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
|
|
6803
|
-
};
|
|
6804
|
-
const supersededNativeGoalAcks = supersedeOtherCodexNativeGoalAcks(process.cwd(), nextMission, ack);
|
|
6805
|
-
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_native_goal_ack', { ack });
|
|
7330
|
+
const { saved, ack, supersededNativeGoalAcks } = recordCodexNativeGoalAck(mission, process.cwd(), { reportedObjective });
|
|
6806
7331
|
const payload = refreshCodexGoalController(process.cwd());
|
|
6807
7332
|
printJsonOrText(
|
|
6808
7333
|
{
|
|
@@ -6927,15 +7452,17 @@ atris mission - durable goal + loop + owner + proof state
|
|
|
6927
7452
|
(rolls up sibling git-worktree missions; --local scopes to this checkout)
|
|
6928
7453
|
atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json] Create a Mission Room card and shareable receipt from messy intent
|
|
6929
7454
|
atris mission prune-runs [--apply] [--days <n>] [--keep-newest <n>] [--json] Compress old run receipts into a manifest and prune unreferenced clutter
|
|
6930
|
-
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]
|
|
6931
7456
|
atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
|
|
6932
7457
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
6933
7458
|
atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
|
|
7459
|
+
atris mission set-runner <id> <runner|engine> [--model <id>] [--json]
|
|
6934
7460
|
atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
|
|
6935
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/
|
|
6936
7462
|
atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
|
|
6937
|
-
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--take-goal-slot]
|
|
6938
|
-
[--
|
|
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]
|
|
6939
7466
|
[--room-auto-run|--no-room-auto-run]
|
|
6940
7467
|
[--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
|
|
6941
7468
|
(bare/member-only run prompts; one-word fuzzy intent starts a new visible-goal mission; --due runs the saved queue)
|
|
@@ -7337,6 +7864,9 @@ function missionCommand(args) {
|
|
|
7337
7864
|
return inspectMission(rest);
|
|
7338
7865
|
case 'tick':
|
|
7339
7866
|
return tickMission(rest);
|
|
7867
|
+
case 'set-runner':
|
|
7868
|
+
case 'runner':
|
|
7869
|
+
return setMissionRunner(rest);
|
|
7340
7870
|
case 'run':
|
|
7341
7871
|
return runMission(rest);
|
|
7342
7872
|
case 'complete':
|
|
@@ -7358,6 +7888,8 @@ function missionCommand(args) {
|
|
|
7358
7888
|
module.exports = {
|
|
7359
7889
|
missionCommand,
|
|
7360
7890
|
inspectMission,
|
|
7891
|
+
expireStaleMissions,
|
|
7892
|
+
reapPausedMissions,
|
|
7361
7893
|
missionHeartbeatLines,
|
|
7362
7894
|
listMissions,
|
|
7363
7895
|
listWorktreeRollupMissions,
|
|
@@ -7367,6 +7899,7 @@ module.exports = {
|
|
|
7367
7899
|
selectDueMission,
|
|
7368
7900
|
selectAtrisGoalMission,
|
|
7369
7901
|
selectCodexGoalMission,
|
|
7902
|
+
codexGoalNextCommand,
|
|
7370
7903
|
usefulClaudeReceiptSummary,
|
|
7371
7904
|
cappedClaudeReceiptText,
|
|
7372
7905
|
extractLayerFromReceiptText,
|
|
@@ -7378,4 +7911,5 @@ module.exports = {
|
|
|
7378
7911
|
detectUnavailableModel,
|
|
7379
7912
|
missionPauseNextAction,
|
|
7380
7913
|
consecutiveSameReasonErrors,
|
|
7914
|
+
missionVerifierTimeoutMs,
|
|
7381
7915
|
};
|