atris 3.32.0 → 3.34.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/README.md +43 -1
- package/atris.md +17 -0
- package/ax +364 -11
- package/bin/atris.js +62 -4
- package/commands/autoland.js +8 -2
- package/commands/engine.js +299 -0
- package/commands/integrations.js +147 -0
- package/commands/loops.js +212 -0
- package/commands/member.js +63 -10
- package/commands/mission.js +163 -18
- package/commands/task.js +173 -14
- package/commands/truth.js +70 -10
- package/lib/fleet.js +356 -0
- package/lib/inspect-fields.js +174 -0
- package/lib/runner-command.js +24 -0
- package/lib/task-db.js +38 -0
- package/package.json +2 -1
- package/scripts/agent_worktree.py +72 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* atris loops - one view of every background loop: what runs, what died,
|
|
3
|
+
* what you can start or stop.
|
|
4
|
+
*
|
|
5
|
+
* atris loops - the board (registry jobs, launchd agents, missions)
|
|
6
|
+
* atris loops stop <id> - disable a registry job or boot out a launchd agent
|
|
7
|
+
* atris loops start <id> - enable a registry job or kickstart a launchd agent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync, spawnSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
function heartbeatDir() {
|
|
16
|
+
return process.env.ATRIS_HEARTBEAT_DIR || path.join(os.homedir(), '.atris', 'heartbeat');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readJson(filePath, fallback) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function registryPath() {
|
|
28
|
+
return path.join(heartbeatDir(), 'registry.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadRegistry() {
|
|
32
|
+
const registry = readJson(registryPath(), null);
|
|
33
|
+
return registry && Array.isArray(registry.jobs) ? registry : { jobs: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadRunState() {
|
|
37
|
+
return readJson(path.join(heartbeatDir(), 'state.json'), {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function agoText(iso) {
|
|
41
|
+
const then = Date.parse(iso || '');
|
|
42
|
+
if (!Number.isFinite(then)) return 'never';
|
|
43
|
+
const mins = Math.max(0, Math.round((Date.now() - then) / 60000));
|
|
44
|
+
if (mins < 60) return `${mins}m ago`;
|
|
45
|
+
const hours = Math.floor(mins / 60);
|
|
46
|
+
if (hours < 48) return `${hours}h ago`;
|
|
47
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function listLaunchdAgents() {
|
|
51
|
+
let raw = '';
|
|
52
|
+
try {
|
|
53
|
+
raw = execSync('launchctl list', { encoding: 'utf8', timeout: 10000 });
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return raw
|
|
58
|
+
.split('\n')
|
|
59
|
+
.filter(line => line.includes('com.atris.'))
|
|
60
|
+
.map(line => {
|
|
61
|
+
const [pid, exitCode, label] = line.trim().split(/\s+/);
|
|
62
|
+
return {
|
|
63
|
+
label,
|
|
64
|
+
running: pid !== '-',
|
|
65
|
+
pid: pid !== '-' ? pid : null,
|
|
66
|
+
lastExit: exitCode,
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
.filter(agent => agent.label);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function workspaceMission(cwd) {
|
|
73
|
+
const goalState = readJson(path.join(cwd, '.atris', 'state', 'atris_goal.json'), null);
|
|
74
|
+
const goal = goalState && goalState.goal;
|
|
75
|
+
if (!goal || !goal.objective) return null;
|
|
76
|
+
const started = Date.parse(goal.created_at || '');
|
|
77
|
+
const elapsedMins = Number.isFinite(started) ? Math.round((Date.now() - started) / 60000) : null;
|
|
78
|
+
return {
|
|
79
|
+
id: goal.mission_id || '?',
|
|
80
|
+
status: goal.mission_status || '?',
|
|
81
|
+
objective: String(goal.objective),
|
|
82
|
+
elapsedMins,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showBoard(cwd) {
|
|
87
|
+
const registry = loadRegistry();
|
|
88
|
+
const state = loadRunState();
|
|
89
|
+
|
|
90
|
+
console.log('background loops');
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
const enabled = registry.jobs.filter(job => job.enabled !== false && job.disabled !== true);
|
|
94
|
+
const disabledCount = registry.jobs.length - enabled.length;
|
|
95
|
+
console.log(`heartbeat registry (${enabled.length} live, ${disabledCount} retired)`);
|
|
96
|
+
if (!enabled.length) {
|
|
97
|
+
console.log(' none declared');
|
|
98
|
+
}
|
|
99
|
+
for (const job of enabled) {
|
|
100
|
+
const run = state[job.id] || {};
|
|
101
|
+
const fails = Number(run.consecutive_fails || 0);
|
|
102
|
+
const health = fails > 2 ? `${fails} consecutive fails` : fails > 0 ? `${fails} recent fail${fails === 1 ? '' : 's'}` : 'healthy';
|
|
103
|
+
const cadence = job.cadence_minutes >= 60 ? `${Math.round(job.cadence_minutes / 60)}h` : `${job.cadence_minutes}m`;
|
|
104
|
+
console.log(` ${String(job.id).padEnd(22)} every ${String(cadence).padEnd(5)} last ${agoText(run.last_run).padEnd(9)} ${health}`);
|
|
105
|
+
console.log(` ${''.padEnd(22)} ${String(job.purpose || '').slice(0, 84)}`);
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
const agents = listLaunchdAgents();
|
|
110
|
+
console.log(`launchd agents (${agents.length})`);
|
|
111
|
+
for (const agent of agents) {
|
|
112
|
+
const status = agent.running ? `running pid ${agent.pid}` : `loaded, last exit ${agent.lastExit}`;
|
|
113
|
+
console.log(` ${agent.label.padEnd(44)} ${status}`);
|
|
114
|
+
}
|
|
115
|
+
if (!agents.length) console.log(' none');
|
|
116
|
+
console.log('');
|
|
117
|
+
|
|
118
|
+
const mission = workspaceMission(cwd);
|
|
119
|
+
console.log('mission (this workspace)');
|
|
120
|
+
if (mission) {
|
|
121
|
+
const elapsed = mission.elapsedMins === null ? '' : ` · ${mission.elapsedMins}m in`;
|
|
122
|
+
console.log(` ${mission.id}`);
|
|
123
|
+
console.log(` ${mission.status}${elapsed} · ${mission.objective.slice(0, 80)}`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(' none active');
|
|
126
|
+
}
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('control: atris loops stop <id> · atris loops start <id>');
|
|
129
|
+
console.log('ids: registry job id, or a com.atris.* launchd label');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function saveRegistry(registry) {
|
|
133
|
+
const target = registryPath();
|
|
134
|
+
fs.copyFileSync(target, `${target}.bak`);
|
|
135
|
+
fs.writeFileSync(target, `${JSON.stringify(registry, null, 2)}\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setRegistryJob(id, enabled) {
|
|
139
|
+
const registry = loadRegistry();
|
|
140
|
+
const job = registry.jobs.find(row => row.id === id);
|
|
141
|
+
if (!job) return false;
|
|
142
|
+
job.enabled = enabled;
|
|
143
|
+
if (enabled) {
|
|
144
|
+
delete job.disabled;
|
|
145
|
+
delete job.disabled_reason;
|
|
146
|
+
} else {
|
|
147
|
+
job.disabled = true;
|
|
148
|
+
job.disabled_reason = `stopped via atris loops ${new Date().toISOString().slice(0, 10)}`;
|
|
149
|
+
}
|
|
150
|
+
saveRegistry(registry);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function launchdControl(label, action) {
|
|
155
|
+
const uid = process.getuid();
|
|
156
|
+
if (action === 'stop') {
|
|
157
|
+
const result = spawnSync('launchctl', ['bootout', `gui/${uid}/${label}`], { encoding: 'utf8' });
|
|
158
|
+
return result.status === 0;
|
|
159
|
+
}
|
|
160
|
+
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
161
|
+
if (!fs.existsSync(plist)) return false;
|
|
162
|
+
const bootstrap = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, plist], { encoding: 'utf8' });
|
|
163
|
+
if (bootstrap.status === 0) return true;
|
|
164
|
+
const kick = spawnSync('launchctl', ['kickstart', '-k', `gui/${uid}/${label}`], { encoding: 'utf8' });
|
|
165
|
+
return kick.status === 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function controlLoop(id, action) {
|
|
169
|
+
const target = String(id || '').trim();
|
|
170
|
+
if (!target) {
|
|
171
|
+
console.error(`Usage: atris loops ${action} <id>`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (target.startsWith('com.atris.')) {
|
|
175
|
+
const ok = launchdControl(target, action);
|
|
176
|
+
if (!ok) {
|
|
177
|
+
console.error(`Could not ${action} ${target}. Check: launchctl print gui/$(id -u)/${target}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
console.log(`${action === 'stop' ? 'Stopped' : 'Started'} ${target}.`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const ok = setRegistryJob(target, action === 'start');
|
|
184
|
+
if (!ok) {
|
|
185
|
+
console.error(`No registry job named "${target}". Run: atris loops`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
console.log(`${action === 'stop' ? 'Disabled' : 'Enabled'} ${target} in the heartbeat registry (backup written).`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function loopsCommand(subcommand, ...args) {
|
|
192
|
+
switch (subcommand) {
|
|
193
|
+
case undefined:
|
|
194
|
+
case 'board':
|
|
195
|
+
case 'list':
|
|
196
|
+
showBoard(process.cwd());
|
|
197
|
+
break;
|
|
198
|
+
case 'stop':
|
|
199
|
+
controlLoop(args[0], 'stop');
|
|
200
|
+
break;
|
|
201
|
+
case 'start':
|
|
202
|
+
controlLoop(args[0], 'start');
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
console.log('Loop commands:');
|
|
206
|
+
console.log(' atris loops - board: registry jobs, launchd agents, mission');
|
|
207
|
+
console.log(' atris loops stop <id> - disable a registry job or stop a launchd agent');
|
|
208
|
+
console.log(' atris loops start <id> - enable a registry job or start a launchd agent');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { loopsCommand, showBoard };
|
package/commands/member.js
CHANGED
|
@@ -611,16 +611,48 @@ function startMemberRunMission(name, missionText, args = []) {
|
|
|
611
611
|
}
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
// atris member ping <name> "<message>" — talk to
|
|
615
|
-
//
|
|
616
|
-
// missions in sibling checkouts)
|
|
614
|
+
// atris member ping <name> "<message>" — talk to a busy member mid-run.
|
|
615
|
+
// Two delivery lanes: the member's most recently touched live mission
|
|
616
|
+
// (including --worktree missions in sibling checkouts), whose next tick
|
|
617
|
+
// consumes the note, and the member's claimed task dialogue, which task-loop
|
|
618
|
+
// agents re-read every step without ever ticking a mission. The ping lands on
|
|
619
|
+
// every lane that exists and errors only when neither does.
|
|
620
|
+
function pingClaimedTaskDialogue(name, text, from) {
|
|
621
|
+
let taskDb;
|
|
622
|
+
try {
|
|
623
|
+
taskDb = require('../lib/task-db');
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const db = taskDb.open();
|
|
629
|
+
const local = taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot(), status: 'claimed', claimedBy: name });
|
|
630
|
+
const pool = local.length ? local : taskDb.listTasks(db, { status: 'claimed', claimedBy: name });
|
|
631
|
+
if (!pool.length) return null;
|
|
632
|
+
const task = [...pool].sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))[0];
|
|
633
|
+
const result = taskDb.noteTask(db, { id: task.id, actor: from, content: text });
|
|
634
|
+
if (!result.noted) return null;
|
|
635
|
+
return { task_id: task.id, title: task.title };
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
617
641
|
function memberPing(name, ...args) {
|
|
618
642
|
if (!name || name === '--help' || name === '-h') {
|
|
619
|
-
console.log('Usage: atris member ping <name> "<message>" [--json]');
|
|
620
|
-
console.log('Leaves
|
|
643
|
+
console.log('Usage: atris member ping <name> "<message>" [--from <who>] [--json]');
|
|
644
|
+
console.log('Leaves the note on the member\'s active mission and claimed task dialogue; whichever loop the member runs reads it next.');
|
|
621
645
|
return;
|
|
622
646
|
}
|
|
623
|
-
const
|
|
647
|
+
const asJson = args.includes('--json');
|
|
648
|
+
const rest = args.filter((a) => a !== '--json');
|
|
649
|
+
let from = process.env.USER || 'operator';
|
|
650
|
+
const fromIdx = rest.indexOf('--from');
|
|
651
|
+
if (fromIdx !== -1) {
|
|
652
|
+
from = rest[fromIdx + 1] || from;
|
|
653
|
+
rest.splice(fromIdx, 2);
|
|
654
|
+
}
|
|
655
|
+
const text = rest.filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
624
656
|
if (!text) {
|
|
625
657
|
console.error('atris member ping: message required');
|
|
626
658
|
process.exit(2);
|
|
@@ -633,12 +665,33 @@ function memberPing(name, ...args) {
|
|
|
633
665
|
]
|
|
634
666
|
.filter((m) => m && m.owner === name && !terminal.has(m.status))
|
|
635
667
|
.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
|
|
636
|
-
|
|
637
|
-
|
|
668
|
+
|
|
669
|
+
const taskNote = pingClaimedTaskDialogue(name, text, from);
|
|
670
|
+
|
|
671
|
+
if (!candidates.length && !taskNote) {
|
|
672
|
+
console.error(`atris member ping: no live mission or claimed task for "${name}". Start one: atris member run ${name} "<objective>"`);
|
|
638
673
|
process.exit(1);
|
|
639
674
|
}
|
|
640
|
-
|
|
641
|
-
|
|
675
|
+
|
|
676
|
+
const mission = candidates.length
|
|
677
|
+
? missionMod.pingMission([candidates[0].id, text, '--from', from], { silent: true })
|
|
678
|
+
: null;
|
|
679
|
+
const pendingPings = mission ? (mission.pings || []).filter((p) => p && !p.consumed_at).length : null;
|
|
680
|
+
|
|
681
|
+
if (asJson) {
|
|
682
|
+
console.log(JSON.stringify({
|
|
683
|
+
ok: true,
|
|
684
|
+
action: 'member_ping',
|
|
685
|
+
member: name,
|
|
686
|
+
mission_id: mission ? mission.id : null,
|
|
687
|
+
pending_pings: pendingPings,
|
|
688
|
+
task_id: taskNote ? taskNote.task_id : null,
|
|
689
|
+
}));
|
|
690
|
+
} else {
|
|
691
|
+
if (mission) console.log(`pinged ${mission.id} — the next tick reads it (${pendingPings} unread).`);
|
|
692
|
+
if (taskNote) console.log(`pinged task ${taskNote.task_id} dialogue — ${name} reads it on the next step.`);
|
|
693
|
+
}
|
|
694
|
+
return { mission, task_note: taskNote };
|
|
642
695
|
}
|
|
643
696
|
|
|
644
697
|
function memberRun(name, ...args) {
|
package/commands/mission.js
CHANGED
|
@@ -30,6 +30,15 @@ const {
|
|
|
30
30
|
formatBytes,
|
|
31
31
|
} = require('../lib/runs-prune');
|
|
32
32
|
const { operatorReady, hasAgentJargon } = require('./autoland');
|
|
33
|
+
const {
|
|
34
|
+
MISSION_INSPECT_FIELDS,
|
|
35
|
+
readFieldsFlag,
|
|
36
|
+
stripInspectArgs,
|
|
37
|
+
validateFields,
|
|
38
|
+
missionInspectFieldValues,
|
|
39
|
+
inspectTextLines,
|
|
40
|
+
buildInspectPayload,
|
|
41
|
+
} = require('../lib/inspect-fields');
|
|
33
42
|
|
|
34
43
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
35
44
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
@@ -194,6 +203,7 @@ function printJsonOrText(payload, lines, asJson) {
|
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
const MISSION_RUN_VALUE_FLAGS = [
|
|
206
|
+
'--slots',
|
|
197
207
|
'--max-ticks',
|
|
198
208
|
'--max-wall',
|
|
199
209
|
'--minutes',
|
|
@@ -213,6 +223,8 @@ const MISSION_RUN_VALUE_FLAGS = [
|
|
|
213
223
|
const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
214
224
|
'--json',
|
|
215
225
|
'--due',
|
|
226
|
+
'--fleet',
|
|
227
|
+
'--dry-run',
|
|
216
228
|
'--no-claude',
|
|
217
229
|
'--no-verify',
|
|
218
230
|
'--complete-on-pass',
|
|
@@ -227,6 +239,7 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
|
227
239
|
'--no-room-auto-run',
|
|
228
240
|
'--allow-native-goal-supersede',
|
|
229
241
|
'--supersede-paused-native-goal',
|
|
242
|
+
'--take-goal-slot',
|
|
230
243
|
'--always-on',
|
|
231
244
|
'--xp-task',
|
|
232
245
|
'--agent-xp',
|
|
@@ -1619,7 +1632,7 @@ function missionFromArgs(args) {
|
|
|
1619
1632
|
'--native-goal-objective',
|
|
1620
1633
|
'--visible-goal-status',
|
|
1621
1634
|
'--visible-goal-objective',
|
|
1622
|
-
], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--allow-native-goal-supersede', '--supersede-paused-native-goal']).join(' ').trim();
|
|
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']).join(' ').trim();
|
|
1623
1636
|
if (!objective) {
|
|
1624
1637
|
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
1625
1638
|
}
|
|
@@ -2765,9 +2778,53 @@ function inheritedWorktreeBase(cwd) {
|
|
|
2765
2778
|
}
|
|
2766
2779
|
}
|
|
2767
2780
|
|
|
2781
|
+
// Dedup gate: the same objective + owner already active anywhere in the
|
|
2782
|
+
// workspace family (this store or any worktree's) is reused, never cloned.
|
|
2783
|
+
// Born 2026-07-02: an hourly alive loop spawned six identical auto-improver
|
|
2784
|
+
// missions in six fresh worktrees in one day — pure token burn. --duplicate
|
|
2785
|
+
// is the explicit escape hatch.
|
|
2786
|
+
const TWIN_ACTIVE_STATUSES = new Set(['planning', 'ready', 'running']);
|
|
2787
|
+
|
|
2788
|
+
function normalizedObjective(text) {
|
|
2789
|
+
return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function findActiveTwinMission(objective, owner, root = process.cwd()) {
|
|
2793
|
+
const wantObjective = normalizedObjective(objective);
|
|
2794
|
+
const wantOwner = String(owner || '').trim().toLowerCase();
|
|
2795
|
+
if (!wantObjective) return null;
|
|
2796
|
+
const candidates = [
|
|
2797
|
+
...listMissions(root),
|
|
2798
|
+
...listWorktreeRollupMissions(root),
|
|
2799
|
+
];
|
|
2800
|
+
for (const m of candidates) {
|
|
2801
|
+
if (!m || !TWIN_ACTIVE_STATUSES.has(m.status)) continue;
|
|
2802
|
+
if (normalizedObjective(m.objective) !== wantObjective) continue;
|
|
2803
|
+
if (String(m.owner || '').trim().toLowerCase() !== wantOwner) continue;
|
|
2804
|
+
return m;
|
|
2805
|
+
}
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2768
2809
|
function startMission(args) {
|
|
2769
2810
|
const asJson = wantsJson(args);
|
|
2770
2811
|
const mission = missionFromArgs(args);
|
|
2812
|
+
if (!hasFlag(args, '--duplicate')) {
|
|
2813
|
+
const twin = findActiveTwinMission(mission.objective, mission.owner);
|
|
2814
|
+
if (twin) {
|
|
2815
|
+
printJsonOrText(
|
|
2816
|
+
{ 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)' },
|
|
2817
|
+
[
|
|
2818
|
+
`Already active: ${twin.id} (${twin.status})`,
|
|
2819
|
+
`Same objective, same owner — reusing it instead of starting a clone.`,
|
|
2820
|
+
`Resume: atris mission run ${twin.id}`,
|
|
2821
|
+
`Really want a second one: re-run with --duplicate`,
|
|
2822
|
+
],
|
|
2823
|
+
asJson,
|
|
2824
|
+
);
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2771
2828
|
// --worktree: bind the mission to its own isolated checkout. We chdir before
|
|
2772
2829
|
// any state writes so the mission record, baseline sidecar, receipts, and
|
|
2773
2830
|
// member files all land inside the worktree — ticks run there, and the main
|
|
@@ -2795,6 +2852,9 @@ function startMission(args) {
|
|
|
2795
2852
|
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
2796
2853
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
2797
2854
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective });
|
|
2855
|
+
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
2856
|
+
? takeCodexGoalSlotForMission(saved, process.cwd())
|
|
2857
|
+
: null;
|
|
2798
2858
|
const memberState = renderMemberMissionState(saved.owner);
|
|
2799
2859
|
const logPath = appendMemberLog(saved.owner, 'Mission started', {
|
|
2800
2860
|
mission: saved.objective,
|
|
@@ -2805,7 +2865,7 @@ function startMission(args) {
|
|
|
2805
2865
|
});
|
|
2806
2866
|
const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
|
|
2807
2867
|
printJsonOrText(
|
|
2808
|
-
{ ok: true, action: 'mission_started', mission: saved, warnings, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath, worktree_baseline: worktreeBaseline ? { path: path.relative(process.cwd(), missionBaselinePath(saved.id)), dirty_count: worktreeBaseline.dirty_count, dirty_hash: worktreeBaseline.dirty_hash } : null },
|
|
2868
|
+
{ ok: true, action: 'mission_started', mission: saved, warnings, goal_slot_handoff: goalSlotHandoff, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath, worktree_baseline: worktreeBaseline ? { path: path.relative(process.cwd(), missionBaselinePath(saved.id)), dirty_count: worktreeBaseline.dirty_count, dirty_hash: worktreeBaseline.dirty_hash } : null },
|
|
2809
2869
|
[
|
|
2810
2870
|
`Started mission: ${saved.objective}`,
|
|
2811
2871
|
`Owner: ${saved.owner}`,
|
|
@@ -2920,6 +2980,9 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2920
2980
|
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
2921
2981
|
...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
|
|
2922
2982
|
};
|
|
2983
|
+
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
2984
|
+
? takeCodexGoalSlotForMission(saved, process.cwd(), nativeGoalOptions)
|
|
2985
|
+
: null;
|
|
2923
2986
|
const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: saved.id });
|
|
2924
2987
|
const codexGoalState = runnerUsesCallerSession(saved.runner)
|
|
2925
2988
|
? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
|
|
@@ -2944,6 +3007,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2944
3007
|
} : null,
|
|
2945
3008
|
completed_continuation_goal: completedContinuationGoal,
|
|
2946
3009
|
direct_goal_request: directGoalRequest,
|
|
3010
|
+
goal_slot_handoff: goalSlotHandoff,
|
|
2947
3011
|
atris_goal_state: atrisGoalState,
|
|
2948
3012
|
codex_goal_state: codexGoalState,
|
|
2949
3013
|
requires_native_goal_start: codexGoalState?.requires_native_goal_start === true || codexGoalState?.goal?.requires_native_goal_start === true,
|
|
@@ -4611,11 +4675,11 @@ function codexGoalObjective(mission) {
|
|
|
4611
4675
|
return mission.objective;
|
|
4612
4676
|
}
|
|
4613
4677
|
|
|
4614
|
-
function codexNativeGoalAck(mission
|
|
4678
|
+
function codexNativeGoalAck(mission) {
|
|
4615
4679
|
const ack = mission?.native_goal_ack || null;
|
|
4616
4680
|
if (!ack || String(ack.runtime || '').toLowerCase() !== 'codex') return null;
|
|
4617
4681
|
if (ack.status !== 'active') return null;
|
|
4618
|
-
if (String(ack.
|
|
4682
|
+
if (ack.mission_id && String(ack.mission_id) !== String(mission?.id || '')) return null;
|
|
4619
4683
|
return ack;
|
|
4620
4684
|
}
|
|
4621
4685
|
|
|
@@ -4821,6 +4885,33 @@ function activeCodexVisibleGoalOwner(root = process.cwd(), excludeId = '', now =
|
|
|
4821
4885
|
return candidates[0] || null;
|
|
4822
4886
|
}
|
|
4823
4887
|
|
|
4888
|
+
function pauseMissionRecord(mission, reason, root = process.cwd()) {
|
|
4889
|
+
const next = {
|
|
4890
|
+
...mission,
|
|
4891
|
+
status: 'paused',
|
|
4892
|
+
paused_at: stampIso(),
|
|
4893
|
+
stop_reason: reason,
|
|
4894
|
+
next_action: `resume with: atris mission tick ${mission.id}`,
|
|
4895
|
+
};
|
|
4896
|
+
const { mission: saved } = saveMission(next, root, 'mission_paused', { reason, source: 'goal_slot_handoff' });
|
|
4897
|
+
appendMemberLog(saved.owner, 'Mission paused', { mission: saved.objective, reason });
|
|
4898
|
+
return saved;
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
function takeCodexGoalSlotForMission(newMission, root = process.cwd(), options = {}) {
|
|
4902
|
+
if (!newMission || !isCodexGoalMission(newMission)) return null;
|
|
4903
|
+
const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
|
|
4904
|
+
const activeOwner = activeCodexVisibleGoalOwner(root, newMission.id, new Date(), runtimeGoalState);
|
|
4905
|
+
if (!activeOwner) return null;
|
|
4906
|
+
const reason = `visible goal replaced by ${newMission.id}`;
|
|
4907
|
+
const paused = pauseMissionRecord(activeOwner, reason, root);
|
|
4908
|
+
return {
|
|
4909
|
+
paused_mission_id: paused.id,
|
|
4910
|
+
paused_mission: missionStatusView(paused),
|
|
4911
|
+
reason,
|
|
4912
|
+
};
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4824
4915
|
function codexGoalActiveConflictPayload(newMission, activeMission, request = null, heartbeatMode = false, runtimeGoalState = null) {
|
|
4825
4916
|
const newObjective = codexGoalObjective(newMission);
|
|
4826
4917
|
const pausedConflict = runtimeGoalState?.status === 'paused';
|
|
@@ -5119,7 +5210,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
5119
5210
|
const taskSpine = missionTaskSpine(mission);
|
|
5120
5211
|
const missionView = missionStatusView(mission);
|
|
5121
5212
|
const objective = codexGoalObjective(mission);
|
|
5122
|
-
const ack = codexNativeGoalAck(mission
|
|
5213
|
+
const ack = codexNativeGoalAck(mission);
|
|
5123
5214
|
const runtimeNeedsReplace = !ack && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
|
|
5124
5215
|
const nativeGoalAction = ack
|
|
5125
5216
|
? null
|
|
@@ -5777,6 +5868,21 @@ async function runMission(args) {
|
|
|
5777
5868
|
help();
|
|
5778
5869
|
return;
|
|
5779
5870
|
}
|
|
5871
|
+
// --fleet: staff every idle capable engine on the board's claimable
|
|
5872
|
+
// safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
|
|
5873
|
+
// full loop — see lib/fleet.js. --dry-run previews the staffing only.
|
|
5874
|
+
if (hasFlag(args, '--fleet')) {
|
|
5875
|
+
const { runFleetFlight } = require('../lib/fleet');
|
|
5876
|
+
const slots = Math.max(1, Number(readFlag(args, '--slots', '')) || 3);
|
|
5877
|
+
const flight = await runFleetFlight({
|
|
5878
|
+
slots,
|
|
5879
|
+
dryRun: hasFlag(args, '--dry-run'),
|
|
5880
|
+
log: asJson ? () => {} : console.log,
|
|
5881
|
+
});
|
|
5882
|
+
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
5883
|
+
process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5780
5886
|
const dueMode = hasFlag(args, '--due');
|
|
5781
5887
|
const skipClaude = hasFlag(args, '--no-claude');
|
|
5782
5888
|
const verifyEach = !hasFlag(args, '--no-verify');
|
|
@@ -6723,16 +6829,15 @@ function ackMissionGoal(args) {
|
|
|
6723
6829
|
releaseMissionLock(lock);
|
|
6724
6830
|
exitMissionError(`Mission "${ref}" uses runner=${mission.runner || 'manual'}; native Codex goal ack is only for runner=codex_goal.`, 2, asJson);
|
|
6725
6831
|
}
|
|
6726
|
-
const
|
|
6727
|
-
const
|
|
6728
|
-
|
|
6729
|
-
releaseMissionLock(lock);
|
|
6730
|
-
exitMissionError(`Native goal objective mismatch. Expected: ${expectedObjective}`, 2, asJson);
|
|
6731
|
-
}
|
|
6832
|
+
const canonicalObjective = codexGoalObjective(mission);
|
|
6833
|
+
const reportedObjective = readFlag(args, '--objective', canonicalObjective);
|
|
6834
|
+
const objective = canonicalObjective;
|
|
6732
6835
|
const ack = {
|
|
6733
6836
|
runtime: 'codex',
|
|
6734
6837
|
status: 'active',
|
|
6838
|
+
mission_id: mission.id,
|
|
6735
6839
|
objective,
|
|
6840
|
+
...(reportedObjective && reportedObjective !== objective ? { reported_objective: reportedObjective } : {}),
|
|
6736
6841
|
acknowledged_at: stampIso(),
|
|
6737
6842
|
};
|
|
6738
6843
|
const nextMission = {
|
|
@@ -6848,13 +6953,15 @@ function help() {
|
|
|
6848
6953
|
console.log(`
|
|
6849
6954
|
atris mission - durable goal + loop + owner + proof state
|
|
6850
6955
|
|
|
6851
|
-
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree]
|
|
6956
|
+
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree] [--take-goal-slot]
|
|
6852
6957
|
[--runner manual|claude|atris2|codex_goal] [--model <id>]
|
|
6853
6958
|
(runner claude spawns local claude -p per tick, --model passes through;
|
|
6854
6959
|
runner atris2 runs each tick as one /atris2/turn on the AtrisOS backend,
|
|
6855
6960
|
default model atris:fast; runner codex_goal publishes the goal for a live
|
|
6856
6961
|
Codex session to pull via atris mission goal)
|
|
6857
6962
|
atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
|
|
6963
|
+
atris mission inspect <id> --fields status,runner,ack,pings [--json]
|
|
6964
|
+
Field-selectable mission state (status, runner, native goal ack, ping counts)
|
|
6858
6965
|
atris mission doctor [--local] [--json] Flag no-verifier missions, help missions, stale ready receipts, and blocked always-on loops
|
|
6859
6966
|
atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
|
|
6860
6967
|
atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
|
|
@@ -6869,8 +6976,9 @@ atris mission - durable goal + loop + owner + proof state
|
|
|
6869
6976
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
6870
6977
|
atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
|
|
6871
6978
|
atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
|
|
6979
|
+
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/
|
|
6872
6980
|
atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
|
|
6873
|
-
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede]
|
|
6981
|
+
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--take-goal-slot]
|
|
6874
6982
|
[--spend-full-budget|--use-whole-budget|--stop-when-done] [--room-preflight|--no-room-preflight]
|
|
6875
6983
|
[--room-auto-run|--no-room-auto-run]
|
|
6876
6984
|
[--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
|
|
@@ -7149,7 +7257,39 @@ function findMissionAcrossWorktrees(ref, root = process.cwd()) {
|
|
|
7149
7257
|
// atris mission ping <id> "<message>" — leave a note the mission's next tick
|
|
7150
7258
|
// reads (and consumes) as operator direction. This is how you talk to an
|
|
7151
7259
|
// always-on member mid-run without stopping it.
|
|
7152
|
-
function
|
|
7260
|
+
function inspectMission(args) {
|
|
7261
|
+
const asJson = wantsJson(args);
|
|
7262
|
+
const parsed = readFieldsFlag(args, '--fields');
|
|
7263
|
+
if (!parsed || parsed.error) {
|
|
7264
|
+
exitMissionError(
|
|
7265
|
+
parsed?.error || 'Usage: atris mission inspect <id> --fields status,runner,ack,pings [--json]',
|
|
7266
|
+
2,
|
|
7267
|
+
asJson,
|
|
7268
|
+
);
|
|
7269
|
+
}
|
|
7270
|
+
const fieldError = validateFields(parsed.fields, MISSION_INSPECT_FIELDS, 'mission');
|
|
7271
|
+
if (fieldError) exitMissionError(fieldError, 2, asJson);
|
|
7272
|
+
const ref = stripInspectArgs(args)[0] || '';
|
|
7273
|
+
if (!ref) {
|
|
7274
|
+
exitMissionError('Usage: atris mission inspect <id> --fields status,runner,ack,pings [--json]', 2, asJson);
|
|
7275
|
+
}
|
|
7276
|
+
const found = findMissionAcrossWorktrees(ref);
|
|
7277
|
+
if (!found) exitMissingMission(ref, 1, asJson);
|
|
7278
|
+
const { mission } = found;
|
|
7279
|
+
const values = missionInspectFieldValues(mission, parsed.fields);
|
|
7280
|
+
const payload = buildInspectPayload({
|
|
7281
|
+
action: 'mission_inspect',
|
|
7282
|
+
idKey: 'mission_id',
|
|
7283
|
+
idValue: mission.id,
|
|
7284
|
+
fields: parsed.fields,
|
|
7285
|
+
values,
|
|
7286
|
+
});
|
|
7287
|
+
printJsonOrText(payload, inspectTextLines(parsed.fields, values), asJson);
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7290
|
+
// always-on member mid-run without stopping it. opts.silent skips console
|
|
7291
|
+
// output so callers (member ping) can compose their own multi-lane report.
|
|
7292
|
+
function pingMission(args, opts = {}) {
|
|
7153
7293
|
const asJson = args.includes('--json');
|
|
7154
7294
|
const rest = args.filter((a) => a !== '--json');
|
|
7155
7295
|
let from = process.env.USER || 'operator';
|
|
@@ -7181,10 +7321,12 @@ function pingMission(args) {
|
|
|
7181
7321
|
{ from, text: text.slice(0, 200) },
|
|
7182
7322
|
).mission;
|
|
7183
7323
|
const pending = (saved.pings || []).filter((p) => p && !p.consumed_at).length;
|
|
7184
|
-
if (
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7324
|
+
if (!opts.silent) {
|
|
7325
|
+
if (asJson) {
|
|
7326
|
+
console.log(JSON.stringify({ ok: true, action: 'mission_ping', mission_id: saved.id, pending_pings: pending, ping }));
|
|
7327
|
+
} else {
|
|
7328
|
+
console.log(`pinged ${saved.id} — the next tick reads it (${pending} unread).`);
|
|
7329
|
+
}
|
|
7188
7330
|
}
|
|
7189
7331
|
return saved;
|
|
7190
7332
|
}
|
|
@@ -7235,6 +7377,8 @@ function missionCommand(args) {
|
|
|
7235
7377
|
return goalLoopMission(rest);
|
|
7236
7378
|
case 'ping':
|
|
7237
7379
|
return pingMission(rest);
|
|
7380
|
+
case 'inspect':
|
|
7381
|
+
return inspectMission(rest);
|
|
7238
7382
|
case 'tick':
|
|
7239
7383
|
return tickMission(rest);
|
|
7240
7384
|
case 'run':
|
|
@@ -7257,6 +7401,7 @@ function missionCommand(args) {
|
|
|
7257
7401
|
|
|
7258
7402
|
module.exports = {
|
|
7259
7403
|
missionCommand,
|
|
7404
|
+
inspectMission,
|
|
7260
7405
|
missionHeartbeatLines,
|
|
7261
7406
|
listMissions,
|
|
7262
7407
|
listWorktreeRollupMissions,
|