@ours.network/fleet 1.0.4 → 1.1.0-nightly.1
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 +41 -50
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/errors.d.ts +1 -1
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +1 -24
- package/dist/application/role-creation-service.d.ts +5 -5
- package/dist/application/role-creation-service.js +5 -7
- package/dist/application/role-repository.d.ts +1 -2
- package/dist/application/role-repository.js +7 -7
- package/dist/application/session-control.d.ts +0 -12
- package/dist/application/session-control.js +0 -42
- package/dist/application/session-mutations.d.ts +5 -5
- package/dist/application/task-room-service.d.ts +37 -2
- package/dist/application/task-room-service.js +49 -7
- package/dist/application/types.d.ts +0 -7
- package/dist/atomic-file.d.ts +5 -3
- package/dist/atomic-file.js +32 -8
- package/dist/briefing.js +1 -4
- package/dist/build-info.json +5 -5
- package/dist/cli.js +9 -23
- package/dist/config.d.ts +7 -7
- package/dist/config.js +13 -34
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +46 -30
- package/dist/doctor.js +0 -7
- package/dist/fleet-proxy.d.ts +1 -1
- package/dist/harness/acp-session-transport.d.ts +8 -0
- package/dist/harness/acp-session-transport.js +3 -0
- package/dist/harness/agent-session.d.ts +33 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +18 -0
- package/dist/harness/claude-code-session.js +28 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +29 -81
- package/dist/harness/codex-session.d.ts +18 -0
- package/dist/harness/codex-session.js +28 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +35 -45
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -3
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +2 -3
- package/dist/model-env.js +2 -3
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/owner-channel/attachments.d.ts +2 -4
- package/dist/owner-channel/attachments.js +6 -39
- package/dist/owner-channel/channel.d.ts +2 -2
- package/dist/owner-channel/channel.js +16 -3
- package/dist/owner-channel/commands.d.ts +18 -1
- package/dist/owner-channel/commands.js +73 -4
- package/dist/rooms-tasks/cli.js +121 -2
- package/dist/rooms-tasks/task-lists.d.ts +19 -0
- package/dist/rooms-tasks/task-lists.js +96 -0
- package/dist/rooms-tasks/task-state.d.ts +3 -0
- package/dist/rooms-tasks/task-state.js +281 -175
- package/dist/rooms-tasks/types.d.ts +11 -1
- package/dist/runner.d.ts +5 -15
- package/dist/runner.js +26 -102
- package/dist/session/acp.d.ts +2 -2
- package/dist/session/acp.js +2 -3
- package/dist/session/activity.d.ts +2 -3
- package/dist/session/activity.js +2 -3
- package/dist/session/arbiter.d.ts +4 -4
- package/dist/session/control.d.ts +3 -3
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.js +8 -9
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.js +6 -4
- package/dist/watchdog/run.js +8 -23
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/runtime.js +6 -15
- package/dist/web/server.d.ts +3 -3
- package/dist/web/server.js +89 -15
- package/dist/web/topology-promote.js +2 -5
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/{index-BCBK78hw.js → index-DhMDVRU1.js} +6 -6
- package/dist/web-app/index.html +2 -2
- package/package.json +3 -8
- package/dist/session/tmux.d.ts +0 -28
- package/dist/session/tmux.js +0 -80
- package/dist/web/terminal/bridge.d.ts +0 -27
- package/dist/web/terminal/bridge.js +0 -317
- package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
- package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
|
@@ -1,11 +1,66 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
5
5
|
import { stateRoot } from '../paths.js';
|
|
6
6
|
import { TASK_TERMINAL_STATES, TASK_CANCELLABLE_STATES } from './types.js';
|
|
7
|
+
import { DEFAULT_TASK_LIST_ID, readTaskLists } from './task-lists.js';
|
|
7
8
|
export const tasksDir = () => join(stateRoot(), 'tasks');
|
|
8
9
|
function taskPath(id) { return join(tasksDir(), `${id}.json`); }
|
|
10
|
+
function taskLockPath(id) { return join(tasksDir(), '.locks', id); }
|
|
11
|
+
const localTaskLocks = new Set();
|
|
12
|
+
function withTaskLock(id, fn) {
|
|
13
|
+
if (localTaskLocks.has(id))
|
|
14
|
+
return fn();
|
|
15
|
+
const path = taskLockPath(id);
|
|
16
|
+
const ownerPath = join(path, 'owner.json');
|
|
17
|
+
const token = randomUUID();
|
|
18
|
+
mkdirSync(join(tasksDir(), '.locks'), { recursive: true });
|
|
19
|
+
for (;;) {
|
|
20
|
+
const claimPath = `${path}.claim.${process.pid}.${randomUUID()}`;
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(claimPath);
|
|
23
|
+
writeFileSync(join(claimPath, 'owner.json'), JSON.stringify({ token, pid: process.pid }));
|
|
24
|
+
renameSync(claimPath, path);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
rmSync(claimPath, { recursive: true, force: true });
|
|
29
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
|
|
30
|
+
throw error;
|
|
31
|
+
let alive = false;
|
|
32
|
+
try {
|
|
33
|
+
const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
34
|
+
if (typeof owner.pid === 'number') {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(owner.pid, 0);
|
|
37
|
+
alive = true;
|
|
38
|
+
}
|
|
39
|
+
catch { /* dead */ }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* corrupt legacy lock */ }
|
|
43
|
+
if (!alive) {
|
|
44
|
+
rmSync(path, { recursive: true, force: true });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
localTaskLocks.add(id);
|
|
51
|
+
try {
|
|
52
|
+
return fn();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
localTaskLocks.delete(id);
|
|
56
|
+
try {
|
|
57
|
+
const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
58
|
+
if (owner.token === token)
|
|
59
|
+
rmSync(path, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
catch { /* ownership cannot be proved */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
9
64
|
function generateTaskId() {
|
|
10
65
|
const ts = Date.now().toString(36).padStart(9, '0');
|
|
11
66
|
const rand = randomUUID().replace(/-/g, '').slice(0, 8);
|
|
@@ -27,11 +82,20 @@ function readTask(id) {
|
|
|
27
82
|
const p = taskPath(id);
|
|
28
83
|
if (!existsSync(p))
|
|
29
84
|
throw new TaskStateError(`task not found: ${id}`);
|
|
30
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
85
|
+
return presentTask(JSON.parse(readFileSync(p, 'utf8')));
|
|
31
86
|
}
|
|
32
87
|
function writeTask(record) {
|
|
33
88
|
mkdirSync(tasksDir(), { recursive: true });
|
|
34
|
-
|
|
89
|
+
const persisted = { ...record };
|
|
90
|
+
delete persisted.list_name;
|
|
91
|
+
replaceFileAtomically(taskPath(record.task_id), JSON.stringify(persisted, null, 2) + '\n');
|
|
92
|
+
}
|
|
93
|
+
function presentTask(record) {
|
|
94
|
+
const listId = record.list_id ?? DEFAULT_TASK_LIST_ID;
|
|
95
|
+
const list = readTaskLists().find(item => item.list_id === listId);
|
|
96
|
+
if (!list)
|
|
97
|
+
throw new TaskStateError(`task ${record.task_id} references missing list ${listId}`);
|
|
98
|
+
return { ...record, list_id: listId, list_name: list.name };
|
|
35
99
|
}
|
|
36
100
|
// ── Lifecycle transitions ───────────────────────────────────────────────
|
|
37
101
|
const VALID_TRANSITIONS = {
|
|
@@ -60,6 +124,7 @@ export function createTask(input) {
|
|
|
60
124
|
const state = input.start === false ? 'backlog' : 'provisioning';
|
|
61
125
|
const record = {
|
|
62
126
|
task_id: generateTaskId(),
|
|
127
|
+
list_id: input.listId ?? DEFAULT_TASK_LIST_ID,
|
|
63
128
|
title: input.title,
|
|
64
129
|
brief: input.brief,
|
|
65
130
|
brief_file: input.brief_file,
|
|
@@ -74,9 +139,9 @@ export function createTask(input) {
|
|
|
74
139
|
started_at: state === 'provisioning' ? new Date().toISOString() : undefined,
|
|
75
140
|
};
|
|
76
141
|
writeTask(record);
|
|
77
|
-
return record;
|
|
142
|
+
return readTask(record.task_id);
|
|
78
143
|
}
|
|
79
|
-
export function getTask(id) { return readTask(id); }
|
|
144
|
+
export function getTask(id) { return withTaskLock(id, () => readTask(id)); }
|
|
80
145
|
export function listTasks(filter) {
|
|
81
146
|
const dir = tasksDir();
|
|
82
147
|
if (!existsSync(dir))
|
|
@@ -87,222 +152,263 @@ export function listTasks(filter) {
|
|
|
87
152
|
const tasks = [];
|
|
88
153
|
for (const f of readdirSync(dir).filter(f => f.endsWith('.json'))) {
|
|
89
154
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
155
|
+
const id = f.slice(0, -5);
|
|
156
|
+
const t = withTaskLock(id, () => presentTask(JSON.parse(readFileSync(join(dir, f), 'utf8'))));
|
|
157
|
+
if ((!states || states.includes(t.state)) && (!filter?.listId || t.list_id === filter.listId))
|
|
92
158
|
tasks.push(t);
|
|
93
159
|
}
|
|
94
160
|
catch { /* corrupt file, skip */ }
|
|
95
161
|
}
|
|
96
|
-
|
|
162
|
+
const order = new Map(readTaskLists().map((item, index) => [item.list_id, index]));
|
|
163
|
+
return tasks.sort((a, b) => (order.get(a.list_id) - order.get(b.list_id))
|
|
164
|
+
|| a.created_at.localeCompare(b.created_at) || a.task_id.localeCompare(b.task_id));
|
|
165
|
+
}
|
|
166
|
+
export function moveTaskToList(id, listId) {
|
|
167
|
+
return withTaskLock(id, () => {
|
|
168
|
+
const task = readTask(id);
|
|
169
|
+
task.list_id = listId;
|
|
170
|
+
writeTask(task);
|
|
171
|
+
return readTask(id);
|
|
172
|
+
});
|
|
97
173
|
}
|
|
98
174
|
export function findByIdempotencyKey(key) {
|
|
99
175
|
return listTasks().find(t => t.idempotency_key === key);
|
|
100
176
|
}
|
|
101
177
|
export function startTask(id) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
178
|
+
return withTaskLock(id, () => {
|
|
179
|
+
const t = readTask(id);
|
|
180
|
+
assertNoPendingTerminalIntent(t);
|
|
181
|
+
assertTransition(t.state, 'provisioning');
|
|
182
|
+
t.state = 'provisioning';
|
|
183
|
+
t.started_at = new Date().toISOString();
|
|
184
|
+
writeTask(t);
|
|
185
|
+
return t;
|
|
186
|
+
});
|
|
109
187
|
}
|
|
110
188
|
export function activateTask(id) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
189
|
+
return withTaskLock(id, () => {
|
|
190
|
+
const t = readTask(id);
|
|
191
|
+
assertNoPendingTerminalIntent(t);
|
|
192
|
+
assertTransition(t.state, 'active');
|
|
193
|
+
t.state = 'active';
|
|
194
|
+
delete t.blocked;
|
|
195
|
+
writeTask(t);
|
|
196
|
+
return t;
|
|
197
|
+
});
|
|
118
198
|
}
|
|
119
199
|
export function blockTask(id, reason) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
200
|
+
return withTaskLock(id, () => {
|
|
201
|
+
const t = readTask(id);
|
|
202
|
+
assertNoPendingTerminalIntent(t);
|
|
203
|
+
if (TASK_TERMINAL_STATES.includes(t.state))
|
|
204
|
+
throw new TaskStateError(`cannot block a '${t.state}' task`);
|
|
205
|
+
t.blocked = { reason, at: new Date().toISOString() };
|
|
206
|
+
writeTask(t);
|
|
207
|
+
return t;
|
|
208
|
+
});
|
|
127
209
|
}
|
|
128
210
|
export function unblockTask(id) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
211
|
+
return withTaskLock(id, () => {
|
|
212
|
+
const t = readTask(id);
|
|
213
|
+
assertNoPendingTerminalIntent(t);
|
|
214
|
+
if (!t.blocked)
|
|
215
|
+
throw new TaskStateError('task is not blocked');
|
|
216
|
+
delete t.blocked;
|
|
217
|
+
writeTask(t);
|
|
218
|
+
return t;
|
|
219
|
+
});
|
|
136
220
|
}
|
|
137
221
|
export function reviewTask(id) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
222
|
+
return withTaskLock(id, () => {
|
|
223
|
+
const t = readTask(id);
|
|
224
|
+
assertNoPendingTerminalIntent(t);
|
|
225
|
+
assertTransition(t.state, 'review');
|
|
226
|
+
t.state = 'review';
|
|
227
|
+
delete t.blocked;
|
|
228
|
+
writeTask(t);
|
|
229
|
+
return t;
|
|
230
|
+
});
|
|
145
231
|
}
|
|
146
232
|
export function completeTask(id, outcome) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
233
|
+
return withTaskLock(id, () => {
|
|
234
|
+
const t = readTask(id);
|
|
235
|
+
assertNoPendingTerminalIntent(t);
|
|
236
|
+
assertTransition(t.state, 'done');
|
|
237
|
+
t.state = 'done';
|
|
238
|
+
t.ended_at = new Date().toISOString();
|
|
239
|
+
delete t.blocked;
|
|
240
|
+
if (outcome)
|
|
241
|
+
t.outcome = outcome;
|
|
242
|
+
writeTask(t);
|
|
243
|
+
return t;
|
|
244
|
+
});
|
|
157
245
|
}
|
|
158
246
|
export function cancelTask(id) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
247
|
+
return withTaskLock(id, () => {
|
|
248
|
+
const t = readTask(id);
|
|
249
|
+
assertNoPendingTerminalIntent(t);
|
|
250
|
+
if (!TASK_CANCELLABLE_STATES.includes(t.state))
|
|
251
|
+
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
252
|
+
t.state = 'cancelled';
|
|
253
|
+
t.ended_at = new Date().toISOString();
|
|
254
|
+
delete t.blocked;
|
|
255
|
+
writeTask(t);
|
|
256
|
+
return t;
|
|
257
|
+
});
|
|
168
258
|
}
|
|
169
259
|
function sameOutcome(left, right) {
|
|
170
260
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
171
261
|
}
|
|
172
262
|
/** Persist the first terminal request. Callers serialize this with the task-operation lock. */
|
|
173
263
|
export function beginTaskTerminalIntent(id, input) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (existing
|
|
178
|
-
|
|
179
|
-
|
|
264
|
+
return withTaskLock(id, () => {
|
|
265
|
+
const t = readTask(id);
|
|
266
|
+
const existing = t.terminal_intent;
|
|
267
|
+
if (existing) {
|
|
268
|
+
if (existing.kind !== input.kind || existing.room_id !== input.roomId
|
|
269
|
+
|| !sameOutcome(existing.outcome, input.outcome)) {
|
|
270
|
+
throw new TaskStateError(`task ${id} already has a conflicting '${existing.kind}' terminal intent`);
|
|
271
|
+
}
|
|
272
|
+
return t;
|
|
273
|
+
}
|
|
274
|
+
if (TASK_TERMINAL_STATES.includes(t.state)) {
|
|
275
|
+
throw new TaskStateError(`task ${id} is already in terminal state '${t.state}'`);
|
|
180
276
|
}
|
|
277
|
+
if (input.kind === 'done' && t.state !== 'review') {
|
|
278
|
+
throw new TaskStateError(`cannot transition from '${t.state}' to 'done'`);
|
|
279
|
+
}
|
|
280
|
+
if (input.kind === 'cancelled' && !TASK_CANCELLABLE_STATES.includes(t.state)) {
|
|
281
|
+
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
282
|
+
}
|
|
283
|
+
if (input.roomId !== undefined && t.room_id !== input.roomId) {
|
|
284
|
+
throw new TaskStateError(`task ${id} is not tied to room ${input.roomId}`);
|
|
285
|
+
}
|
|
286
|
+
t.terminal_intent = {
|
|
287
|
+
kind: input.kind,
|
|
288
|
+
status: 'pending',
|
|
289
|
+
room_id: input.roomId,
|
|
290
|
+
outcome: input.outcome,
|
|
291
|
+
accepted_at: new Date().toISOString(),
|
|
292
|
+
};
|
|
293
|
+
writeTask(t);
|
|
181
294
|
return t;
|
|
182
|
-
}
|
|
183
|
-
if (TASK_TERMINAL_STATES.includes(t.state)) {
|
|
184
|
-
throw new TaskStateError(`task ${id} is already in terminal state '${t.state}'`);
|
|
185
|
-
}
|
|
186
|
-
if (input.kind === 'done' && t.state !== 'review') {
|
|
187
|
-
throw new TaskStateError(`cannot transition from '${t.state}' to 'done'`);
|
|
188
|
-
}
|
|
189
|
-
if (input.kind === 'cancelled' && !TASK_CANCELLABLE_STATES.includes(t.state)) {
|
|
190
|
-
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
191
|
-
}
|
|
192
|
-
if (input.roomId !== undefined && t.room_id !== input.roomId) {
|
|
193
|
-
throw new TaskStateError(`task ${id} is not tied to room ${input.roomId}`);
|
|
194
|
-
}
|
|
195
|
-
t.terminal_intent = {
|
|
196
|
-
kind: input.kind,
|
|
197
|
-
status: 'pending',
|
|
198
|
-
room_id: input.roomId,
|
|
199
|
-
outcome: input.outcome,
|
|
200
|
-
accepted_at: new Date().toISOString(),
|
|
201
|
-
};
|
|
202
|
-
writeTask(t);
|
|
203
|
-
return t;
|
|
295
|
+
});
|
|
204
296
|
}
|
|
205
297
|
/** Record an actionable failure without claiming the requested terminal state. */
|
|
206
298
|
export function setTaskTerminalIntentError(id, error, recoveryHint) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
299
|
+
return withTaskLock(id, () => {
|
|
300
|
+
const t = readTask(id);
|
|
301
|
+
if (!t.terminal_intent)
|
|
302
|
+
throw new TaskStateError(`task ${id} has no terminal intent`);
|
|
303
|
+
if (t.terminal_intent.status === 'settled')
|
|
304
|
+
return t;
|
|
305
|
+
t.terminal_intent.first_failure ??= error;
|
|
306
|
+
t.terminal_intent.first_recovery_hint ??= recoveryHint;
|
|
307
|
+
t.terminal_intent.error = error;
|
|
308
|
+
const previousErrorAt = Date.parse(t.terminal_intent.error_at ?? '');
|
|
309
|
+
t.terminal_intent.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
|
|
310
|
+
t.terminal_intent.recovery_hint = recoveryHint;
|
|
311
|
+
writeTask(t);
|
|
211
312
|
return t;
|
|
212
|
-
|
|
213
|
-
t.terminal_intent.first_recovery_hint ??= recoveryHint;
|
|
214
|
-
t.terminal_intent.error = error;
|
|
215
|
-
const previousErrorAt = Date.parse(t.terminal_intent.error_at ?? '');
|
|
216
|
-
t.terminal_intent.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
|
|
217
|
-
t.terminal_intent.recovery_hint = recoveryHint;
|
|
218
|
-
writeTask(t);
|
|
219
|
-
return t;
|
|
313
|
+
});
|
|
220
314
|
}
|
|
221
315
|
/** Atomically publish the task terminal state and settled audit cursor. */
|
|
222
316
|
export function finishTaskTerminalIntent(id) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
317
|
+
return withTaskLock(id, () => {
|
|
318
|
+
const t = readTask(id);
|
|
319
|
+
const intent = t.terminal_intent;
|
|
320
|
+
if (!intent)
|
|
321
|
+
throw new TaskStateError(`task ${id} has no terminal intent`);
|
|
322
|
+
if (intent.status === 'settled')
|
|
323
|
+
return t;
|
|
324
|
+
if (TASK_TERMINAL_STATES.includes(t.state)) {
|
|
325
|
+
throw new TaskStateError(`task ${id} reached terminal state '${t.state}' outside its intent`);
|
|
326
|
+
}
|
|
327
|
+
if (intent.kind === 'done')
|
|
328
|
+
assertTransition(t.state, 'done');
|
|
329
|
+
else if (!TASK_CANCELLABLE_STATES.includes(t.state)) {
|
|
330
|
+
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
331
|
+
}
|
|
332
|
+
t.state = intent.kind;
|
|
333
|
+
t.ended_at = new Date().toISOString();
|
|
334
|
+
delete t.blocked;
|
|
335
|
+
if (intent.outcome)
|
|
336
|
+
t.outcome = intent.outcome;
|
|
337
|
+
intent.status = 'settled';
|
|
338
|
+
intent.settled_at = t.ended_at;
|
|
339
|
+
delete intent.error;
|
|
340
|
+
delete intent.error_at;
|
|
341
|
+
delete intent.recovery_hint;
|
|
342
|
+
writeTask(t);
|
|
228
343
|
return t;
|
|
229
|
-
|
|
230
|
-
throw new TaskStateError(`task ${id} reached terminal state '${t.state}' outside its intent`);
|
|
231
|
-
}
|
|
232
|
-
if (intent.kind === 'done')
|
|
233
|
-
assertTransition(t.state, 'done');
|
|
234
|
-
else if (!TASK_CANCELLABLE_STATES.includes(t.state)) {
|
|
235
|
-
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
236
|
-
}
|
|
237
|
-
t.state = intent.kind;
|
|
238
|
-
t.ended_at = new Date().toISOString();
|
|
239
|
-
delete t.blocked;
|
|
240
|
-
if (intent.outcome)
|
|
241
|
-
t.outcome = intent.outcome;
|
|
242
|
-
intent.status = 'settled';
|
|
243
|
-
intent.settled_at = t.ended_at;
|
|
244
|
-
delete intent.error;
|
|
245
|
-
delete intent.error_at;
|
|
246
|
-
delete intent.recovery_hint;
|
|
247
|
-
writeTask(t);
|
|
248
|
-
return t;
|
|
344
|
+
});
|
|
249
345
|
}
|
|
250
346
|
export function failTask(id, error) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
347
|
+
return withTaskLock(id, () => {
|
|
348
|
+
const t = readTask(id);
|
|
349
|
+
assertNoPendingTerminalIntent(t);
|
|
350
|
+
assertTransition(t.state, 'failed');
|
|
351
|
+
t.state = 'failed';
|
|
352
|
+
t.ended_at = new Date().toISOString();
|
|
353
|
+
t.outcome = { summary: error };
|
|
354
|
+
delete t.blocked;
|
|
355
|
+
writeTask(t);
|
|
356
|
+
return t;
|
|
357
|
+
});
|
|
260
358
|
}
|
|
261
359
|
export function updateTaskRoom(id, roomId, roomIdentityCid) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
360
|
+
return withTaskLock(id, () => {
|
|
361
|
+
const t = readTask(id);
|
|
362
|
+
assertNoPendingTerminalIntent(t);
|
|
363
|
+
t.room_id = roomId;
|
|
364
|
+
t.room_identity_cid = roomIdentityCid;
|
|
365
|
+
writeTask(t);
|
|
366
|
+
return t;
|
|
367
|
+
});
|
|
268
368
|
}
|
|
269
369
|
export function updateTaskTemplate(id, template) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
370
|
+
return withTaskLock(id, () => {
|
|
371
|
+
const t = readTask(id);
|
|
372
|
+
assertNoPendingTerminalIntent(t);
|
|
373
|
+
t.template = template;
|
|
374
|
+
writeTask(t);
|
|
375
|
+
return t;
|
|
376
|
+
});
|
|
275
377
|
}
|
|
276
378
|
export function updateTaskMembers(id, members) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
379
|
+
return withTaskLock(id, () => {
|
|
380
|
+
const t = readTask(id);
|
|
381
|
+
assertNoPendingTerminalIntent(t);
|
|
382
|
+
t.member_roles = members;
|
|
383
|
+
writeTask(t);
|
|
384
|
+
return t;
|
|
385
|
+
});
|
|
282
386
|
}
|
|
283
387
|
/** Remove a completed task from Fleet's backlog. Missing tasks are an idempotent no-op. */
|
|
284
388
|
export function deleteTask(id) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
389
|
+
return withTaskLock(id, () => {
|
|
390
|
+
assertCanonicalTaskId(id);
|
|
391
|
+
const p = taskPath(id);
|
|
392
|
+
let task;
|
|
393
|
+
try {
|
|
394
|
+
task = JSON.parse(readFileSync(p, 'utf8'));
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (isNotFoundError(error))
|
|
398
|
+
return false;
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
if (task.state !== 'done') {
|
|
402
|
+
throw new TaskStateError(`cannot delete a '${task.state}' task; only 'done' tasks can be deleted`);
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
unlinkSync(p);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (isNotFoundError(error))
|
|
409
|
+
return false;
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
308
414
|
}
|
|
@@ -24,7 +24,7 @@ export interface TaskMemberRole {
|
|
|
24
24
|
cowork_role: string;
|
|
25
25
|
}
|
|
26
26
|
export interface TaskOrigin {
|
|
27
|
-
type: 'cli' | 'owner_channel';
|
|
27
|
+
type: 'cli' | 'owner_channel' | 'web';
|
|
28
28
|
owner_cid?: string;
|
|
29
29
|
}
|
|
30
30
|
export interface TaskOutcome {
|
|
@@ -46,6 +46,10 @@ export interface TaskTerminalIntent {
|
|
|
46
46
|
}
|
|
47
47
|
export interface TaskRecord {
|
|
48
48
|
task_id: string;
|
|
49
|
+
/** Stable organizational list identifier. Missing legacy values mean `default`. */
|
|
50
|
+
list_id: string;
|
|
51
|
+
/** Derived presentation field; repositories must never persist it. */
|
|
52
|
+
list_name: string;
|
|
49
53
|
title: string;
|
|
50
54
|
brief?: string;
|
|
51
55
|
brief_file?: string;
|
|
@@ -64,6 +68,12 @@ export interface TaskRecord {
|
|
|
64
68
|
outcome?: TaskOutcome;
|
|
65
69
|
terminal_intent?: TaskTerminalIntent;
|
|
66
70
|
}
|
|
71
|
+
export interface TaskListRecord {
|
|
72
|
+
list_id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
built_in: boolean;
|
|
75
|
+
created_at: string;
|
|
76
|
+
}
|
|
67
77
|
export type RoomOrchestrationState = 'provisioning' | 'active' | 'closing' | 'closed';
|
|
68
78
|
export type SagaPhase = 'persist_intent' | 'create_room' | 'attach_owner' | 'create_members' | 'join_role_groups' | 'wait_seats' | 'launch_work' | 'activate' | 'completed' | 'failed';
|
|
69
79
|
export interface SagaCursor {
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { type ResolvedRole } from './config.js';
|
|
2
|
-
import type { Launch } from './harness/types.js';
|
|
3
|
-
import { Tmux } from './tmux.js';
|
|
4
2
|
import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
|
|
5
3
|
import { type Exec } from './exec.js';
|
|
6
|
-
import { type AcpSessionOptions } from './session/acp.js';
|
|
7
4
|
import { RoleControlServer } from './session/control.js';
|
|
8
|
-
import type {
|
|
5
|
+
import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
|
|
6
|
+
import type { AgentSessionAdapter, AgentSessionStartOptions } from './harness/agent-session.js';
|
|
9
7
|
import { type OwnerChannelHandle, type OwnerChannelOptions } from './owner-channel/channel.js';
|
|
10
8
|
import { type OwnerBinderLease } from './owner-channel/binder.js';
|
|
11
9
|
export interface RunnerDeps {
|
|
12
|
-
tmux: Tmux;
|
|
13
10
|
exec: Exec;
|
|
14
11
|
cpuDelegated(): boolean;
|
|
15
12
|
isAlive(pid: number): boolean;
|
|
@@ -23,9 +20,10 @@ export interface RunnerDeps {
|
|
|
23
20
|
/** Construct trusted owner ingress (injectable for lifecycle tests). */
|
|
24
21
|
createOwnerChannel(opts: OwnerChannelOptions): OwnerChannelHandle;
|
|
25
22
|
/** Start the ACP transport (injectable for deterministic runner lifecycle tests). */
|
|
26
|
-
|
|
23
|
+
/** Neutral construction seam for deterministic runner lifecycle tests. */
|
|
24
|
+
startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
|
|
27
25
|
/** Construct the authenticated role control route (injectable where sockets are unavailable). */
|
|
28
|
-
createControlServer(stateDir: string, session:
|
|
26
|
+
createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
|
|
29
27
|
/** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
|
|
30
28
|
acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
|
|
31
29
|
/** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
|
|
@@ -51,14 +49,6 @@ export declare function harnessChildEnv(role: ResolvedRole, launchEnv: Record<st
|
|
|
51
49
|
* responsible for.
|
|
52
50
|
*/
|
|
53
51
|
export declare function recordMonitorOwner(dir: string, owner: 'fleet' | 'native'): boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Compose the tmux pane shell command: env prefix + argv + exit-status capture.
|
|
56
|
-
* `paneArgv` defaults to `launch.argv`; when isolation is active the caller passes
|
|
57
|
-
* the sandbox-wrapped argv (e.g. `bwrap … -- claude …`). The `env` prefix and the
|
|
58
|
-
* `echo $? > exitfile` capture stay host-side, outside the sandbox, so the runner
|
|
59
|
-
* still sees the real exit code.
|
|
60
|
-
*/
|
|
61
|
-
export declare function buildPaneCommand(launch: Launch, roleEnv: Record<string, string> | undefined, exitStatusPath: string, paneArgv?: string[]): string;
|
|
62
52
|
/**
|
|
63
53
|
* Read the pane's `.exit-status`. Three shapes are accepted: the structured
|
|
64
54
|
* record written above, a bare number left by a pre-upgrade pane (so an
|