@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,10 +1,11 @@
|
|
|
1
1
|
import { type FleetConfig } from '../config.js';
|
|
2
2
|
import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
3
3
|
import { provisionMembers } from '../rooms-tasks/provision.js';
|
|
4
|
-
import
|
|
4
|
+
import { moveTaskToList } from '../rooms-tasks/task-state.js';
|
|
5
|
+
import type { RoomOrchestrationRecord, TaskListRecord, TaskOrigin, TaskOutcome, TaskRecord, TaskState } from '../rooms-tasks/types.js';
|
|
5
6
|
export type TaskRoomActor = {
|
|
6
7
|
kind: 'local_control';
|
|
7
|
-
surface: 'cli';
|
|
8
|
+
surface: 'cli' | 'web';
|
|
8
9
|
} | {
|
|
9
10
|
kind: 'authenticated_owner';
|
|
10
11
|
surface: 'messenger';
|
|
@@ -55,6 +56,7 @@ export interface CreateTaskRequest {
|
|
|
55
56
|
noRoom?: boolean;
|
|
56
57
|
idempotencyKey?: string;
|
|
57
58
|
origin: TaskOrigin;
|
|
59
|
+
list?: string;
|
|
58
60
|
}
|
|
59
61
|
export interface CreateRoomRequest {
|
|
60
62
|
actor: TaskRoomActor;
|
|
@@ -69,6 +71,7 @@ export interface TaskRoomServiceDeps {
|
|
|
69
71
|
cowork?(config: FleetConfig): CoworkAdapter;
|
|
70
72
|
binPath?(): string;
|
|
71
73
|
provisionMembers?: typeof provisionMembers;
|
|
74
|
+
moveTaskToList?: typeof moveTaskToList;
|
|
72
75
|
}
|
|
73
76
|
/** Exact extraction of the previously CLI-owned task create/start behavior. */
|
|
74
77
|
export declare class TaskRoomApplicationService {
|
|
@@ -84,7 +87,39 @@ export declare class TaskRoomApplicationService {
|
|
|
84
87
|
}): Promise<TaskRecord>;
|
|
85
88
|
listTasks(filter?: {
|
|
86
89
|
state?: TaskState | TaskState[];
|
|
90
|
+
list?: string;
|
|
87
91
|
}): TaskRecord[];
|
|
92
|
+
listTaskLists(): TaskListRecord[];
|
|
93
|
+
groupedTasks(filter?: {
|
|
94
|
+
state?: TaskState | TaskState[];
|
|
95
|
+
list?: string;
|
|
96
|
+
}): Array<{
|
|
97
|
+
list: TaskListRecord;
|
|
98
|
+
tasks: TaskRecord[];
|
|
99
|
+
}>;
|
|
100
|
+
createTaskList(input: {
|
|
101
|
+
actor: TaskRoomActor;
|
|
102
|
+
name: string;
|
|
103
|
+
}): Promise<TaskListRecord>;
|
|
104
|
+
renameTaskList(input: {
|
|
105
|
+
actor: TaskRoomActor;
|
|
106
|
+
name: string;
|
|
107
|
+
newName: string;
|
|
108
|
+
}): Promise<TaskListRecord>;
|
|
109
|
+
moveTask(input: {
|
|
110
|
+
actor: TaskRoomActor;
|
|
111
|
+
taskId: string;
|
|
112
|
+
list: string;
|
|
113
|
+
}): Promise<TaskRecord>;
|
|
114
|
+
deleteTaskList(input: {
|
|
115
|
+
actor: TaskRoomActor;
|
|
116
|
+
name: string;
|
|
117
|
+
destination?: string;
|
|
118
|
+
}): Promise<{
|
|
119
|
+
deleted: TaskListRecord;
|
|
120
|
+
moved: number;
|
|
121
|
+
destination?: TaskListRecord;
|
|
122
|
+
}>;
|
|
88
123
|
getTask(taskId: string): {
|
|
89
124
|
task: TaskRecord;
|
|
90
125
|
orchestration: RoomOrchestrationRecord | undefined;
|
|
@@ -3,7 +3,8 @@ import { ConfigError, loadConfig } from '../config.js';
|
|
|
3
3
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
4
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
5
5
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
6
|
-
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
|
|
6
|
+
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
|
|
7
|
+
import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
|
|
7
8
|
import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
8
9
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
9
10
|
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
|
|
@@ -34,11 +35,13 @@ export class TaskRoomApplicationService {
|
|
|
34
35
|
name: template.name, version: template.version, content_hash: template.content_hash,
|
|
35
36
|
};
|
|
36
37
|
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
37
|
-
let task =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
let task = await withTaskListsLock(() => {
|
|
39
|
+
const list = resolveTaskList(request.list ?? 'default');
|
|
40
|
+
return persistTask({
|
|
41
|
+
title: request.title, brief, brief_file: request.briefFile, template: ref,
|
|
42
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
43
|
+
start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
|
|
44
|
+
});
|
|
42
45
|
});
|
|
43
46
|
if (!request.backlog && !request.noRoom && ref && !task.room_id) {
|
|
44
47
|
try {
|
|
@@ -82,7 +85,46 @@ export class TaskRoomApplicationService {
|
|
|
82
85
|
return task;
|
|
83
86
|
}
|
|
84
87
|
listTasks(filter) {
|
|
85
|
-
|
|
88
|
+
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
89
|
+
return readTasks({ state: filter?.state, listId });
|
|
90
|
+
}
|
|
91
|
+
listTaskLists() { return readTaskLists(); }
|
|
92
|
+
groupedTasks(filter) {
|
|
93
|
+
const tasks = this.listTasks(filter);
|
|
94
|
+
const lists = filter?.list ? [resolveTaskList(filter.list)] : readTaskLists();
|
|
95
|
+
return lists.map(list => ({ list, tasks: tasks.filter(task => task.list_id === list.list_id) }));
|
|
96
|
+
}
|
|
97
|
+
async createTaskList(input) {
|
|
98
|
+
return withTaskListsLock(() => createTaskListLocked(input.name));
|
|
99
|
+
}
|
|
100
|
+
async renameTaskList(input) {
|
|
101
|
+
return withTaskListsLock(() => renameTaskListLocked(input.name, input.newName));
|
|
102
|
+
}
|
|
103
|
+
async moveTask(input) {
|
|
104
|
+
return withTaskListsLock(() => {
|
|
105
|
+
const destination = resolveTaskList(input.list);
|
|
106
|
+
return (this.deps.moveTaskToList ?? moveTaskToList)(input.taskId, destination.list_id);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async deleteTaskList(input) {
|
|
110
|
+
return withTaskListsLock(() => {
|
|
111
|
+
const source = resolveTaskList(input.name);
|
|
112
|
+
if (source.list_id === DEFAULT_TASK_LIST_ID)
|
|
113
|
+
throw new TaskListError('default_immutable', "the 'default' list cannot be deleted");
|
|
114
|
+
const assigned = readTasks({ listId: source.list_id });
|
|
115
|
+
let destination;
|
|
116
|
+
if (assigned.length) {
|
|
117
|
+
if (!input.destination)
|
|
118
|
+
throw new TaskListError('destination_required', `task list '${source.name}' is not empty; destination is required`);
|
|
119
|
+
destination = resolveTaskList(input.destination);
|
|
120
|
+
if (destination.list_id === source.list_id)
|
|
121
|
+
throw new TaskListError('same_destination', 'deletion destination must differ from the source list');
|
|
122
|
+
for (const task of assigned)
|
|
123
|
+
(this.deps.moveTaskToList ?? moveTaskToList)(task.task_id, destination.list_id);
|
|
124
|
+
}
|
|
125
|
+
deleteTaskListRecordLocked(source.list_id);
|
|
126
|
+
return { deleted: source, moved: assigned.length, destination };
|
|
127
|
+
});
|
|
86
128
|
}
|
|
87
129
|
getTask(taskId) {
|
|
88
130
|
const task = readTask(taskId);
|
|
@@ -117,7 +117,6 @@ export interface RoleCapabilities {
|
|
|
117
117
|
};
|
|
118
118
|
input: {
|
|
119
119
|
text: boolean;
|
|
120
|
-
rawKeys: boolean;
|
|
121
120
|
interrupt: boolean;
|
|
122
121
|
steering: boolean;
|
|
123
122
|
};
|
|
@@ -125,12 +124,6 @@ export interface RoleCapabilities {
|
|
|
125
124
|
observe: boolean;
|
|
126
125
|
respond: boolean;
|
|
127
126
|
};
|
|
128
|
-
terminal: {
|
|
129
|
-
available: boolean;
|
|
130
|
-
reason?: 'wrong_backend' | 'pty_unavailable' | 'offline' | 'incompatible_runner';
|
|
131
|
-
multiViewer: boolean;
|
|
132
|
-
writerLease: boolean;
|
|
133
|
-
};
|
|
134
127
|
lifecycle: {
|
|
135
128
|
start: boolean;
|
|
136
129
|
stop: boolean;
|
package/dist/atomic-file.d.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
export interface LockDeps {
|
|
11
11
|
now?(): number;
|
|
12
12
|
sleep?(ms: number): Promise<void>;
|
|
13
|
+
/** Test hook after a complete private claim is prepared, before atomic publication. */
|
|
14
|
+
beforePublish?(): void | Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
/** The syscalls a replace depends on, injectable so faults can be forced deterministically. */
|
|
15
17
|
export interface WriteDeps {
|
|
@@ -18,9 +20,9 @@ export interface WriteDeps {
|
|
|
18
20
|
closeSync?(fd: number): void;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
|
-
* Run `fn` holding a cross-process lock.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* Run `fn` holding a cross-process lock. A fully populated private claim directory
|
|
24
|
+
* is atomically renamed to the canonical path, so contenders never observe partial
|
|
25
|
+
* ownership metadata; its timestamp lets a crashed holder be reclaimed safely.
|
|
24
26
|
* Same strategy as the runner's launch gate, factored out so both use one.
|
|
25
27
|
*
|
|
26
28
|
* The lock is always released, including when `fn` throws.
|
package/dist/atomic-file.js
CHANGED
|
@@ -4,9 +4,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
const DEFAULT_STALE_MS = 10_000;
|
|
5
5
|
const POLL_MS = 25;
|
|
6
6
|
/**
|
|
7
|
-
* Run `fn` holding a cross-process lock.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Run `fn` holding a cross-process lock. A fully populated private claim directory
|
|
8
|
+
* is atomically renamed to the canonical path, so contenders never observe partial
|
|
9
|
+
* ownership metadata; its timestamp lets a crashed holder be reclaimed safely.
|
|
10
10
|
* Same strategy as the runner's launch gate, factored out so both use one.
|
|
11
11
|
*
|
|
12
12
|
* The lock is always released, including when `fn` throws.
|
|
@@ -15,15 +15,22 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
|
|
|
15
15
|
const now = deps.now ?? (() => Date.now());
|
|
16
16
|
const sleep = deps.sleep ?? ((ms) => new Promise(r => { setTimeout(r, ms); }));
|
|
17
17
|
const stampPath = join(lockPath, 'ts');
|
|
18
|
+
const ownerPath = join(lockPath, 'owner.json');
|
|
19
|
+
const token = randomUUID();
|
|
18
20
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
19
21
|
for (let waited = 0;;) {
|
|
22
|
+
const claimPath = `${lockPath}.claim.${process.pid}.${randomUUID()}`;
|
|
20
23
|
try {
|
|
21
|
-
mkdirSync(
|
|
22
|
-
writeFileSync(
|
|
24
|
+
mkdirSync(claimPath);
|
|
25
|
+
writeFileSync(join(claimPath, 'ts'), String(now()));
|
|
26
|
+
writeFileSync(join(claimPath, 'owner.json'), JSON.stringify({ token, pid: process.pid }));
|
|
27
|
+
await deps.beforePublish?.();
|
|
28
|
+
renameSync(claimPath, lockPath);
|
|
23
29
|
break;
|
|
24
30
|
}
|
|
25
31
|
catch (e) {
|
|
26
|
-
|
|
32
|
+
rmSync(claimPath, { recursive: true, force: true });
|
|
33
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(e.code ?? ''))
|
|
27
34
|
throw e;
|
|
28
35
|
let held = null;
|
|
29
36
|
try {
|
|
@@ -31,7 +38,19 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
|
|
|
31
38
|
held = Number.isFinite(n) ? n : null;
|
|
32
39
|
}
|
|
33
40
|
catch { /* holder died between mkdir and stamp */ }
|
|
34
|
-
|
|
41
|
+
let ownerAlive = false;
|
|
42
|
+
try {
|
|
43
|
+
const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
44
|
+
if (typeof owner.pid === 'number') {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(owner.pid, 0);
|
|
47
|
+
ownerAlive = true;
|
|
48
|
+
}
|
|
49
|
+
catch { /* dead owner */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch { /* legacy or incomplete lock */ }
|
|
53
|
+
if (!ownerAlive && ((held !== null && now() - held > staleMs) || waited > staleMs * 2)) {
|
|
35
54
|
rmSync(lockPath, { recursive: true, force: true }); // break a dead holder's lock
|
|
36
55
|
continue;
|
|
37
56
|
}
|
|
@@ -43,7 +62,12 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
|
|
|
43
62
|
return await fn();
|
|
44
63
|
}
|
|
45
64
|
finally {
|
|
46
|
-
|
|
65
|
+
try {
|
|
66
|
+
const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
67
|
+
if (owner.token === token)
|
|
68
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
catch { /* never remove a lock whose ownership cannot be proved */ }
|
|
47
71
|
}
|
|
48
72
|
}
|
|
49
73
|
/**
|
package/dist/briefing.js
CHANGED
|
@@ -190,9 +190,6 @@ export function generateBriefing(role, v, opts) {
|
|
|
190
190
|
L.push(`7. Await messages. When the monitor wakes you (or the owner requests a manual check),`);
|
|
191
191
|
L.push(` call **${v.getMessagesTool}**, act on them,`);
|
|
192
192
|
L.push(` and reply with ${v.sendTool}. No coordinator is configured — the owner drives you`);
|
|
193
|
-
// NOT `tmux attach -t <name>`: each role's pane lives on its own tmux
|
|
194
|
-
// socket (#32), so a bare attach finds no server. `ours-fleet attach`
|
|
195
|
-
// addresses the right one.
|
|
196
193
|
L.push(` via \`ours-fleet attach ${role.name}\` or by messaging "${id}".`);
|
|
197
194
|
}
|
|
198
195
|
if (role.oversee?.length) {
|
|
@@ -221,7 +218,7 @@ export function generateBriefing(role, v, opts) {
|
|
|
221
218
|
L.push('idle or stalled from `readiness=idle` alone.');
|
|
222
219
|
L.push('');
|
|
223
220
|
L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
|
|
224
|
-
L.push('with `ours-fleet send <Name> "<text>"
|
|
221
|
+
L.push('with `ours-fleet send <Name> "<text>"`; idle with work');
|
|
225
222
|
L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
|
|
226
223
|
L.push('stall. Escalate over ours messaging only when you cannot resolve it yourself.');
|
|
227
224
|
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
5
|
-
"dirty":
|
|
6
|
-
"builtAt": "2026-08-
|
|
2
|
+
"version": "1.1.0-nightly.1",
|
|
3
|
+
"buildId": "bf3c406ba57c",
|
|
4
|
+
"commit": "10d6da8bfd9903d216bce80a9ef89a42c282f8d9",
|
|
5
|
+
"dirty": true,
|
|
6
|
+
"builtAt": "2026-08-30T07:44:27.551Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,6 @@ import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './
|
|
|
11
11
|
import { findRole, loadConfig } from './config.js';
|
|
12
12
|
import { formatDuration } from './duration.js';
|
|
13
13
|
import { resolvedPlan } from './resolved-plan.js';
|
|
14
|
-
import { Tmux, tmuxArgs } from './tmux.js';
|
|
15
14
|
import { pickBackend } from './supervisor/index.js';
|
|
16
15
|
import { up, down } from './ops.js';
|
|
17
16
|
import { readRestartLedger, runSupervised, runTemp } from './runner.js';
|
|
@@ -74,7 +73,7 @@ const passthrough = (cmd, args) => new Promise(resolve => {
|
|
|
74
73
|
});
|
|
75
74
|
const program = new Command()
|
|
76
75
|
.name('ours-fleet')
|
|
77
|
-
.description('Fleet of persistent, identity-bound AI agents — selectable
|
|
76
|
+
.description('Fleet of persistent, identity-bound AI agents — selectable harnesses over ACP sessions.')
|
|
78
77
|
.enablePositionalOptions()
|
|
79
78
|
.version(VERSION);
|
|
80
79
|
const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'config file (default: ~/fleet.yaml + ~/fleet.d/)');
|
|
@@ -369,15 +368,11 @@ cOpt(program.command('force-restart [names...]').description('re-sync + bounce F
|
|
|
369
368
|
});
|
|
370
369
|
program.command('ls').description('list running fleet sessions')
|
|
371
370
|
.action(async () => {
|
|
372
|
-
// Each session has its own tmux server (#32), so there is no single server
|
|
373
|
-
// to ask: the known role names ARE the list of servers to poll.
|
|
374
|
-
const names = [];
|
|
375
371
|
const acp = [];
|
|
376
372
|
for (const root of [agentsRoot(), tmpRoot()]) {
|
|
377
373
|
if (!existsSync(root))
|
|
378
374
|
continue;
|
|
379
375
|
for (const name of readdirSync(root)) {
|
|
380
|
-
names.push(name);
|
|
381
376
|
const stateDir = joinPath(root, name);
|
|
382
377
|
if (!existsSync(controlSocketPath(stateDir)))
|
|
383
378
|
continue;
|
|
@@ -395,14 +390,13 @@ program.command('ls').description('list running fleet sessions')
|
|
|
395
390
|
catch { /* ignore stale sockets */ }
|
|
396
391
|
}
|
|
397
392
|
}
|
|
398
|
-
|
|
399
|
-
console.log([tmux, ...acp].filter(Boolean).join('\n') || '(none)');
|
|
393
|
+
console.log(acp.join('\n') || '(none)');
|
|
400
394
|
});
|
|
401
395
|
program.command('attach <name>').description('open the live console (Ctrl-b d to leave)')
|
|
402
396
|
.action(async (name) => {
|
|
403
397
|
const stateDir = acpStateDir(name);
|
|
404
398
|
if (!stateDir)
|
|
405
|
-
|
|
399
|
+
throw new SessionControlError('offline', 'agent session is offline');
|
|
406
400
|
try {
|
|
407
401
|
const { socket, send } = await followControl(stateDir, message => {
|
|
408
402
|
if ('event' in message)
|
|
@@ -439,7 +433,7 @@ program.command('attach <name>').description('open the live console (Ctrl-b d to
|
|
|
439
433
|
die(e);
|
|
440
434
|
}
|
|
441
435
|
});
|
|
442
|
-
/**
|
|
436
|
+
/** Preserve typed control failures and classify only definite missing-session errors as offline. */
|
|
443
437
|
const asControlError = (e) => {
|
|
444
438
|
if (e instanceof SessionControlError)
|
|
445
439
|
return e;
|
|
@@ -467,23 +461,17 @@ program.command('peek <name> [lines]').description('pane snapshot without attach
|
|
|
467
461
|
for (const event of events.slice(-(lines ? Number(lines) : 40)))
|
|
468
462
|
renderSessionEvent(event);
|
|
469
463
|
}
|
|
470
|
-
else
|
|
471
|
-
|
|
472
|
-
}
|
|
464
|
+
else
|
|
465
|
+
throw new SessionControlError('offline', 'agent session is offline');
|
|
473
466
|
}
|
|
474
467
|
catch (e) {
|
|
475
468
|
die(controlFailure(name, 'peek', e));
|
|
476
469
|
}
|
|
477
470
|
});
|
|
478
471
|
program.command('send <name> [text...]').description("type into the agent's console")
|
|
479
|
-
.option('--key <key>', 'send a raw key instead (Escape, Up, C-c, ...)')
|
|
480
472
|
.action(async (name, text, opts) => {
|
|
481
473
|
const stateDir = acpStateDir(name);
|
|
482
|
-
if (
|
|
483
|
-
die('--key is available only for tmux sessions');
|
|
484
|
-
if (!stateDir && !opts.key && !text?.length)
|
|
485
|
-
die('nothing to send: give text or --key');
|
|
486
|
-
if (stateDir && !text?.length)
|
|
474
|
+
if (!text?.length)
|
|
487
475
|
die('nothing to send: give text');
|
|
488
476
|
try {
|
|
489
477
|
if (stateDir) {
|
|
@@ -496,10 +484,8 @@ program.command('send <name> [text...]').description("type into the agent's cons
|
|
|
496
484
|
? `queued for ${name} behind ${queued.queuedBehind} running turn(s)`
|
|
497
485
|
: `queued for ${name}`);
|
|
498
486
|
}
|
|
499
|
-
else if (opts.key)
|
|
500
|
-
await new Tmux().sendKey(name, opts.key);
|
|
501
487
|
else
|
|
502
|
-
|
|
488
|
+
throw new SessionControlError('offline', 'agent session is offline');
|
|
503
489
|
}
|
|
504
490
|
catch (e) {
|
|
505
491
|
die(controlFailure(name, 'send', e, asControlError(e).kind === 'timeout'
|
|
@@ -1126,7 +1112,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1126
1112
|
.option('--role <name>', 'role name (alternative to the positional name)')
|
|
1127
1113
|
.option('--temp', 'temporary: independent transient supervisor, archived on retirement, gone on reboot')
|
|
1128
1114
|
.option('--harness <id>', 'harness adapter (default: defaults.harness)')
|
|
1129
|
-
.option('--session <backend>', 'session backend:
|
|
1115
|
+
.option('--session <backend>', 'session backend: acp (default: acp)')
|
|
1130
1116
|
.option('--mission <text>', 'one-line mission')
|
|
1131
1117
|
.option('--mission-file <path>', 'UTF-8 mission text (mutually exclusive with --mission)')
|
|
1132
1118
|
.option('--identity <name>', 'ours identity to bind (default: role name)')
|
package/dist/config.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare const NOTIFY_EVENT_TYPES: readonly ["message_received", "file_rec
|
|
|
25
25
|
export type NotifyEventType = (typeof NOTIFY_EVENT_TYPES)[number];
|
|
26
26
|
export type InjectMode = 'notification' | 'full';
|
|
27
27
|
export type MonitorMode = 'fleet' | 'native';
|
|
28
|
-
export type SessionBackendId = '
|
|
28
|
+
export type SessionBackendId = 'acp';
|
|
29
29
|
/** Public, harness-neutral permission policy. */
|
|
30
30
|
export type FleetPermissionMode = 'ask' | 'auto' | 'allow';
|
|
31
31
|
/** `deny` is a deprecated, fail-closed compatibility alias retained for old fleet files. */
|
|
@@ -44,9 +44,6 @@ export interface SessionOptions {
|
|
|
44
44
|
/** ACP agent command and arguments. Defaults are supplied by the harness adapter. */
|
|
45
45
|
command?: string | string[];
|
|
46
46
|
};
|
|
47
|
-
tmux?: {
|
|
48
|
-
boot_grace_ms?: number;
|
|
49
|
-
};
|
|
50
47
|
}
|
|
51
48
|
/** Resolved per-role supervisor-monitor config. */
|
|
52
49
|
export interface MonitorConfig {
|
|
@@ -93,12 +90,14 @@ export interface OwnerAttachmentConfig {
|
|
|
93
90
|
max_file_bytes: number;
|
|
94
91
|
max_request_bytes: number;
|
|
95
92
|
retention_ms: number;
|
|
96
|
-
allowed_mime: string[];
|
|
97
93
|
}
|
|
94
|
+
type OwnerAttachmentConfigInput = Partial<OwnerAttachmentConfig> & {
|
|
95
|
+
/** @deprecated Accepted only so existing configs keep loading; ignored. */
|
|
96
|
+
allowed_mime?: unknown;
|
|
97
|
+
};
|
|
98
98
|
export type OwnerChannelConfigInput = Omit<Partial<OwnerChannelConfig>, 'attachments'> & {
|
|
99
|
-
attachments?:
|
|
99
|
+
attachments?: OwnerAttachmentConfigInput;
|
|
100
100
|
};
|
|
101
|
-
export declare const DEFAULT_OWNER_ATTACHMENT_MIME: readonly ["application/pdf", "application/json", "text/plain", "image/png", "image/jpeg", "image/gif", "image/webp", "audio/ogg", "audio/mpeg", "audio/wav", "audio/x-wav", "audio/mp4", "audio/webm", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation"];
|
|
102
101
|
/** Default wake sources when a role does not list its own. */
|
|
103
102
|
export declare const DEFAULT_WAKE_SOURCES: NotifyEventType[];
|
|
104
103
|
/**
|
|
@@ -236,3 +235,4 @@ export declare function resolveMonitorConfig(defMonitor: unknown, roleMonitor?:
|
|
|
236
235
|
name?: string;
|
|
237
236
|
}): MonitorConfig;
|
|
238
237
|
export declare function findRole(cfg: FleetConfig, name: string): ResolvedRole;
|
|
238
|
+
export {};
|
package/dist/config.js
CHANGED
|
@@ -21,15 +21,6 @@ export const NOTIFY_EVENT_TYPES = [
|
|
|
21
21
|
'message_received', 'file_received', 'sibling_contact_added', 'local_contact_request',
|
|
22
22
|
'pending_message', 'contact_restored', 'inbound_error', 'state_import_failed',
|
|
23
23
|
];
|
|
24
|
-
export const DEFAULT_OWNER_ATTACHMENT_MIME = [
|
|
25
|
-
'application/pdf', 'application/json', 'text/plain',
|
|
26
|
-
'image/png', 'image/jpeg', 'image/gif', 'image/webp',
|
|
27
|
-
'audio/ogg', 'audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/mp4', 'audio/webm',
|
|
28
|
-
'application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint',
|
|
29
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
30
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
31
|
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
32
|
-
];
|
|
33
24
|
/** Default wake sources when a role does not list its own. */
|
|
34
25
|
export const DEFAULT_WAKE_SOURCES = ['message_received', 'file_received', 'local_contact_request', 'pending_message'];
|
|
35
26
|
const MONITOR_KEYS = [
|
|
@@ -410,13 +401,6 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
410
401
|
const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
|
|
411
402
|
if (maxRequestBytes < maxFileBytes)
|
|
412
403
|
throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.max_request_bytes must be at least max_file_bytes`);
|
|
413
|
-
const allowedMime = attachments.allowed_mime ?? [...DEFAULT_OWNER_ATTACHMENT_MIME];
|
|
414
|
-
if (!Array.isArray(allowedMime) || allowedMime.length < 1 || allowedMime.length > 64
|
|
415
|
-
|| allowedMime.some(mime => typeof mime !== 'string'
|
|
416
|
-
|| !/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(mime)))
|
|
417
|
-
throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.allowed_mime must contain 1-64 lowercase MIME types`);
|
|
418
|
-
if (new Set(allowedMime).size !== allowedMime.length)
|
|
419
|
-
throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.allowed_mime must not contain duplicates`);
|
|
420
404
|
if (session !== 'acp')
|
|
421
405
|
throw new ConfigError(`${file}: role '${name}' owner_channel requires session: acp for correlated final replies`);
|
|
422
406
|
return {
|
|
@@ -434,7 +418,6 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
434
418
|
max_file_bytes: maxFileBytes,
|
|
435
419
|
max_request_bytes: maxRequestBytes,
|
|
436
420
|
retention_ms: attachments.retention_ms ?? 24 * 60 * 60 * 1_000,
|
|
437
|
-
allowed_mime: [...allowedMime],
|
|
438
421
|
},
|
|
439
422
|
};
|
|
440
423
|
}
|
|
@@ -541,9 +524,12 @@ export function resolveWorklogPolicy(defaults, role, file = 'config', name = 'ro
|
|
|
541
524
|
return merged;
|
|
542
525
|
}
|
|
543
526
|
function resolveSession(raw, file, name) {
|
|
544
|
-
const value = raw ?? '
|
|
545
|
-
if (value
|
|
546
|
-
throw new ConfigError(`${file}: role '${name}' session:
|
|
527
|
+
const value = raw ?? 'acp';
|
|
528
|
+
if (value === 'tmux')
|
|
529
|
+
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
530
|
+
+ 'with the Codex or Claude Code adapter');
|
|
531
|
+
if (value !== 'acp')
|
|
532
|
+
throw new ConfigError(`${file}: role '${name}' session: must be: acp`);
|
|
547
533
|
return value;
|
|
548
534
|
}
|
|
549
535
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -558,33 +544,26 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
558
544
|
...((defaults?.acp) ?? {}),
|
|
559
545
|
...(role?.acp ?? {}),
|
|
560
546
|
},
|
|
561
|
-
tmux: {
|
|
562
|
-
...((defaults?.tmux) ?? {}),
|
|
563
|
-
...(role?.tmux ?? {}),
|
|
564
|
-
},
|
|
565
547
|
};
|
|
566
|
-
|
|
548
|
+
if (defaults?.tmux !== undefined
|
|
549
|
+
|| role?.tmux !== undefined)
|
|
550
|
+
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
551
|
+
+ 'use session: acp with session_options.acp');
|
|
552
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
567
553
|
if (bad.length)
|
|
568
554
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
569
|
-
if (!isPlainObject(merged.acp)
|
|
555
|
+
if (!isPlainObject(merged.acp))
|
|
570
556
|
throw new ConfigError(`${file}: role '${name}' session_options.${session} must be a map`);
|
|
571
557
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
572
|
-
const tmuxBad = Object.keys(merged.tmux).filter(k => k !== 'boot_grace_ms');
|
|
573
558
|
if (acpBad.length)
|
|
574
559
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
575
|
-
if (tmuxBad.length)
|
|
576
|
-
throw new ConfigError(`${file}: role '${name}' session_options.tmux: unknown key(s) ${tmuxBad.join(', ')}`);
|
|
577
560
|
const command = merged.acp.command;
|
|
578
561
|
if (command !== undefined
|
|
579
562
|
&& !(typeof command === 'string' && command.trim())
|
|
580
563
|
&& !(Array.isArray(command) && command.length > 0
|
|
581
564
|
&& command.every(v => typeof v === 'string' && v.length > 0)))
|
|
582
565
|
throw new ConfigError(`${file}: role '${name}' session_options.acp.command must be a non-empty string or string list`);
|
|
583
|
-
|
|
584
|
-
if (grace !== undefined
|
|
585
|
-
&& (typeof grace !== 'number' || !Number.isFinite(grace) || grace < 0))
|
|
586
|
-
throw new ConfigError(`${file}: role '${name}' session_options.tmux.boot_grace_ms must be a non-negative number`);
|
|
587
|
-
return Object.keys(merged.acp).length || Object.keys(merged.tmux).length ? merged : undefined;
|
|
566
|
+
return Object.keys(merged.acp).length ? merged : undefined;
|
|
588
567
|
}
|
|
589
568
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
590
569
|
if (defaults !== undefined && !isPlainObject(defaults))
|