@ours.network/fleet 0.18.1 → 0.19.0-nightly.10

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.
Files changed (90) hide show
  1. package/README.md +86 -90
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +5 -5
  7. package/dist/cli.js +12 -37
  8. package/dist/config.d.ts +14 -6
  9. package/dist/config.js +58 -31
  10. package/dist/creation.d.ts +38 -22
  11. package/dist/creation.js +111 -24
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +71 -94
  14. package/dist/doctor.d.ts +5 -1
  15. package/dist/doctor.js +143 -12
  16. package/dist/fleet-proxy.d.ts +0 -5
  17. package/dist/harness/acp-agent.js +6 -11
  18. package/dist/harness/claude-code.js +11 -200
  19. package/dist/harness/codex.d.ts +1 -4
  20. package/dist/harness/codex.js +12 -70
  21. package/dist/harness/types.d.ts +4 -54
  22. package/dist/loops/manager.d.ts +1 -30
  23. package/dist/loops/manager.js +6 -69
  24. package/dist/loops/state.d.ts +0 -18
  25. package/dist/loops/state.js +0 -4
  26. package/dist/monitor.js +1 -1
  27. package/dist/ops.d.ts +3 -0
  28. package/dist/ops.js +8 -3
  29. package/dist/owner-channel/attachments.d.ts +25 -2
  30. package/dist/owner-channel/attachments.js +61 -5
  31. package/dist/owner-channel/channel.d.ts +36 -30
  32. package/dist/owner-channel/channel.js +338 -291
  33. package/dist/owner-channel/commands.d.ts +9 -0
  34. package/dist/owner-channel/commands.js +393 -0
  35. package/dist/owner-channel/message-recovery.d.ts +25 -0
  36. package/dist/owner-channel/message-recovery.js +114 -0
  37. package/dist/owner-channel/notices.d.ts +0 -7
  38. package/dist/owner-channel/notices.js +0 -9
  39. package/dist/owner-channel/ours-client.d.ts +148 -0
  40. package/dist/owner-channel/ours-client.js +231 -0
  41. package/dist/rooms-tasks/cli.d.ts +4 -0
  42. package/dist/rooms-tasks/cli.js +1102 -0
  43. package/dist/rooms-tasks/close.d.ts +21 -0
  44. package/dist/rooms-tasks/close.js +159 -0
  45. package/dist/rooms-tasks/config.d.ts +15 -0
  46. package/dist/rooms-tasks/config.js +174 -0
  47. package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
  48. package/dist/rooms-tasks/cowork-adapter.js +272 -0
  49. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  50. package/dist/rooms-tasks/external-worker.js +40 -0
  51. package/dist/rooms-tasks/index.d.ts +10 -0
  52. package/dist/rooms-tasks/index.js +10 -0
  53. package/dist/rooms-tasks/provision.d.ts +21 -0
  54. package/dist/rooms-tasks/provision.js +243 -0
  55. package/dist/rooms-tasks/room-state.d.ts +28 -0
  56. package/dist/rooms-tasks/room-state.js +221 -0
  57. package/dist/rooms-tasks/task-state.d.ts +43 -0
  58. package/dist/rooms-tasks/task-state.js +277 -0
  59. package/dist/rooms-tasks/templates.d.ts +6 -0
  60. package/dist/rooms-tasks/templates.js +94 -0
  61. package/dist/rooms-tasks/terminal.d.ts +23 -0
  62. package/dist/rooms-tasks/terminal.js +53 -0
  63. package/dist/rooms-tasks/types.d.ts +187 -0
  64. package/dist/rooms-tasks/types.js +20 -0
  65. package/dist/runner.d.ts +0 -48
  66. package/dist/runner.js +94 -236
  67. package/dist/session/acp.d.ts +0 -104
  68. package/dist/session/acp.js +10 -213
  69. package/dist/session/conversation-normalizer.d.ts +0 -6
  70. package/dist/session/conversation-normalizer.js +10 -153
  71. package/dist/session/conversation-types.d.ts +4 -23
  72. package/dist/session/types.d.ts +0 -35
  73. package/dist/spawn.js +26 -33
  74. package/dist/supervisor/systemd.js +29 -2
  75. package/dist/temp-lifecycle.d.ts +9 -2
  76. package/dist/temp-lifecycle.js +61 -0
  77. package/dist/watchdog/briefing.js +0 -7
  78. package/dist/watchdog/run.js +3 -3
  79. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  80. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  81. package/dist/web-app/index.html +1 -1
  82. package/dist/worklog.d.ts +1 -7
  83. package/dist/worklog.js +39 -191
  84. package/package.json +3 -1
  85. package/dist/model-env.d.ts +0 -71
  86. package/dist/model-env.js +0 -106
  87. package/dist/owner-channel/mcp.d.ts +0 -24
  88. package/dist/owner-channel/mcp.js +0 -145
  89. package/dist/session/activity.d.ts +0 -31
  90. package/dist/session/activity.js +0 -48
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Typed client for the ours-cowork v1 private management-socket protocol.
3
+ *
4
+ * Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
5
+ * by Cowork's Unix socket and projects the response into orchestration data.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join, resolve } from 'node:path';
10
+ import { createConnection } from 'node:net';
11
+ const RPC_VERSION = 1;
12
+ const DEFAULT_TIMEOUT_MS = 10_000;
13
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
14
+ export class CoworkUnavailableError extends Error {
15
+ constructor(message = 'Cowork management socket is not reachable', options) {
16
+ super(message, options);
17
+ this.name = 'CoworkUnavailableError';
18
+ }
19
+ }
20
+ export class CoworkProtocolError extends Error {
21
+ operation;
22
+ code;
23
+ constructor(operation, message, code = 'protocol_error', options) {
24
+ super(`cowork ${operation}: ${message}`, options);
25
+ this.operation = operation;
26
+ this.code = code;
27
+ this.name = 'CoworkProtocolError';
28
+ }
29
+ }
30
+ function object(value, operation, label) {
31
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
32
+ throw new CoworkProtocolError(operation, `${label} must be an object`);
33
+ return value;
34
+ }
35
+ function string(value, operation, label) {
36
+ if (typeof value !== 'string' || value.length === 0)
37
+ throw new CoworkProtocolError(operation, `${label} must be a non-empty string`);
38
+ return value;
39
+ }
40
+ function text(value, operation, label) {
41
+ if (typeof value !== 'string')
42
+ throw new CoworkProtocolError(operation, `${label} must be a string`);
43
+ return value;
44
+ }
45
+ function roomState(value, operation) {
46
+ if (value !== 'provisioning' && value !== 'active' && value !== 'closing' && value !== 'closed')
47
+ throw new CoworkProtocolError(operation, 'room state is invalid');
48
+ return value;
49
+ }
50
+ function seatState(value, operation) {
51
+ if (value !== 'pending' && value !== 'active' && value !== 'removed')
52
+ throw new CoworkProtocolError(operation, 'seat state is invalid');
53
+ return value;
54
+ }
55
+ function projectSeat(value, operation) {
56
+ const seat = object(value, operation, 'seat');
57
+ return {
58
+ identity_cid: string(seat.identity, operation, 'seat.identity'),
59
+ role: string(seat.role, operation, 'seat.role'),
60
+ seat_state: seatState(seat.state, operation),
61
+ };
62
+ }
63
+ function projectRoom(value, operation) {
64
+ const room = object(value, operation, 'room');
65
+ if (!Array.isArray(room.seats))
66
+ throw new CoworkProtocolError(operation, 'room.seats must be an array');
67
+ const mission = room.mission === undefined ? undefined : object(room.mission, operation, 'room.mission');
68
+ return {
69
+ room_id: string(room.room_id, operation, 'room.room_id'),
70
+ identity_name: string(room.identity_name, operation, 'room.identity_name'),
71
+ // Cowork reserves an empty CID only for its durable packet_pending
72
+ // recovery sentinel; list/show must preserve that recoverable state.
73
+ identity_cid: text(room.identity_cid, operation, 'room.identity_cid'),
74
+ room_name: string(room.room_name, operation, 'room.room_name'),
75
+ state: roomState(room.state, operation),
76
+ seats: room.seats.map((seat) => projectSeat(seat, operation)),
77
+ ...(typeof mission?.goal === 'string' ? { goal: mission.goal } : {}),
78
+ ...(typeof mission?.briefing === 'string' ? { briefing: mission.briefing } : {}),
79
+ };
80
+ }
81
+ /** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
82
+ export function resolveCoworkSocketPath(options = {}) {
83
+ if (options.socketPath)
84
+ return resolve(options.socketPath);
85
+ const env = options.env ?? process.env;
86
+ const home = options.home ?? homedir();
87
+ const configuredPath = options.configPath ?? env.OURS_COWORK_CONFIG;
88
+ const configPath = resolve(configuredPath ?? join(home, '.ours-cowork', 'config.json'));
89
+ let stateDir = resolve(home, '.ours-cowork');
90
+ if (existsSync(configPath)) {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(readFileSync(configPath, 'utf8'));
94
+ }
95
+ catch (error) {
96
+ throw new CoworkProtocolError('config', `malformed config at ${configPath}`, 'invalid_config', { cause: error });
97
+ }
98
+ const config = object(parsed, 'config', 'config');
99
+ if (config.version !== 1 || typeof config.stateDir !== 'string' || !config.stateDir)
100
+ throw new CoworkProtocolError('config', `invalid Cowork v1 config at ${configPath}`, 'invalid_config');
101
+ stateDir = resolve(config.stateDir);
102
+ }
103
+ else if (configuredPath !== undefined) {
104
+ throw new CoworkProtocolError('config', `configured Cowork config does not exist: ${configPath}`, 'invalid_config');
105
+ }
106
+ if (env.OURS_COWORK_STATE_DIR)
107
+ stateDir = resolve(env.OURS_COWORK_STATE_DIR);
108
+ return join(stateDir, 'management.sock');
109
+ }
110
+ function rpcCall(socketPath, method, params, timeoutMs, connect) {
111
+ const id = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
112
+ const request = `${JSON.stringify({ version: RPC_VERSION, id, method, params })}\n`;
113
+ return new Promise((resolveCall, rejectCall) => {
114
+ const socket = connect(socketPath);
115
+ let bytes = '';
116
+ let size = 0;
117
+ let settled = false;
118
+ let connected = false;
119
+ const finishError = (error) => {
120
+ if (settled)
121
+ return;
122
+ settled = true;
123
+ clearTimeout(timer);
124
+ socket.destroy();
125
+ rejectCall(error);
126
+ };
127
+ const timer = setTimeout(() => finishError(new CoworkUnavailableError(connected ? 'Cowork did not answer the management socket' : 'Cowork management socket is not reachable')), timeoutMs);
128
+ socket.setEncoding('utf8');
129
+ socket.once('connect', () => {
130
+ connected = true;
131
+ socket.write(request);
132
+ });
133
+ socket.on('data', (chunk) => {
134
+ if (settled)
135
+ return;
136
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
137
+ size += Buffer.byteLength(text);
138
+ if (size > MAX_RESPONSE_BYTES) {
139
+ finishError(new CoworkProtocolError(method, 'response exceeded 4 MiB'));
140
+ return;
141
+ }
142
+ bytes += text;
143
+ const newline = bytes.indexOf('\n');
144
+ if (newline < 0)
145
+ return;
146
+ if (bytes.slice(newline + 1).trim() !== '') {
147
+ finishError(new CoworkProtocolError(method, 'daemon returned more than one response'));
148
+ return;
149
+ }
150
+ let response;
151
+ try {
152
+ response = object(JSON.parse(bytes.slice(0, newline)), method, 'RPC response');
153
+ }
154
+ catch (error) {
155
+ finishError(error instanceof CoworkProtocolError
156
+ ? error : new CoworkProtocolError(method, 'daemon returned malformed JSON', 'protocol_error', { cause: error }));
157
+ return;
158
+ }
159
+ if (response.version !== RPC_VERSION || response.id !== id
160
+ || (Object.hasOwn(response, 'error') === Object.hasOwn(response, 'result'))) {
161
+ finishError(new CoworkProtocolError(method, 'daemon returned an invalid RPC response'));
162
+ return;
163
+ }
164
+ settled = true;
165
+ clearTimeout(timer);
166
+ socket.destroy();
167
+ if (Object.hasOwn(response, 'error')) {
168
+ const error = object(response.error, method, 'RPC error');
169
+ rejectCall(new CoworkProtocolError(method, typeof error.message === 'string' ? error.message : 'unknown RPC error', typeof error.code === 'string' ? error.code : 'rpc_error'));
170
+ }
171
+ else {
172
+ resolveCall(response.result);
173
+ }
174
+ });
175
+ socket.once('end', () => {
176
+ if (!settled)
177
+ finishError(new CoworkProtocolError(method, 'daemon closed without a complete response'));
178
+ });
179
+ socket.once('error', (error) => {
180
+ if (!settled)
181
+ finishError(new CoworkUnavailableError(error.code === 'EACCES' || error.code === 'EPERM'
182
+ ? 'Cowork management socket access denied' : 'Cowork management socket is not reachable', { cause: error }));
183
+ });
184
+ });
185
+ }
186
+ export function createCoworkAdapter(options = {}) {
187
+ const socketPath = resolveCoworkSocketPath(options);
188
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
189
+ const connect = options.connect ?? ((path) => createConnection(path));
190
+ const call = (method, params) => rpcCall(socketPath, method, params, timeoutMs, connect);
191
+ return {
192
+ async available() {
193
+ try {
194
+ await call('room.list', {});
195
+ return true;
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ },
201
+ async createRoom(opts) {
202
+ const result = projectRoom(await call('room.create', {
203
+ name: opts.room_name,
204
+ goal: opts.goal,
205
+ briefing: opts.briefing,
206
+ ...(opts.quiet_membership === undefined ? {} : { quiet_membership: opts.quiet_membership }),
207
+ ...(opts.anonymous === undefined ? {} : { anonymous: opts.anonymous }),
208
+ }), 'room.create');
209
+ if (!result.identity_cid)
210
+ throw new CoworkProtocolError('room.create', 'created room did not establish an identity CID');
211
+ return { room_id: result.room_id, identity_name: result.identity_name, identity_cid: result.identity_cid };
212
+ },
213
+ async acceptInvite(roomId, invite, opts) {
214
+ const receipt = object(await call('room.accept', {
215
+ room_id: roomId,
216
+ role: opts.role,
217
+ invite,
218
+ expected_cid: opts.expected_cid,
219
+ }), 'room.accept', 'receipt');
220
+ const state = seatState(receipt.state, 'room.accept');
221
+ if (state === 'removed')
222
+ throw new CoworkProtocolError('room.accept', 'accepted seat cannot be removed');
223
+ return {
224
+ seat_cid: string(receipt.identity, 'room.accept', 'receipt.identity'),
225
+ seat_state: state,
226
+ };
227
+ },
228
+ async issueInvite(roomId, opts) {
229
+ const receipt = object(await call('room.invite', {
230
+ room_id: roomId,
231
+ mode: 'public',
232
+ role: opts.role,
233
+ min_accepts: opts.min_accepts,
234
+ }), 'room.invite', 'receipt');
235
+ const invite = object(receipt.invite, 'room.invite', 'receipt.invite');
236
+ return {
237
+ invite: string(receipt.blob, 'room.invite', 'receipt.blob'),
238
+ min_accepts: typeof invite.min_accepts === 'number'
239
+ ? invite.min_accepts : opts.min_accepts,
240
+ };
241
+ },
242
+ async getRoom(roomId) {
243
+ try {
244
+ return projectRoom(await call('room.show', { room_id: roomId }), 'room.show');
245
+ }
246
+ catch (error) {
247
+ if (error instanceof CoworkProtocolError && error.code === 'not_found')
248
+ return undefined;
249
+ throw error;
250
+ }
251
+ },
252
+ async listRooms() {
253
+ const result = await call('room.list', {});
254
+ if (!Array.isArray(result))
255
+ throw new CoworkProtocolError('room.list', 'result must be an array');
256
+ return result.map((room) => projectRoom(room, 'room.list'));
257
+ },
258
+ async closeRoom(roomId) { await call('room.close', { room_id: roomId }); },
259
+ async getSeats(roomId) {
260
+ const result = await call('room.participants', { room_id: roomId });
261
+ if (!Array.isArray(result))
262
+ throw new CoworkProtocolError('room.participants', 'result must be an array');
263
+ return result.map((seat) => projectSeat(seat, 'room.participants'));
264
+ },
265
+ async recoverRoom(roomId) {
266
+ // Cowork performs packet/state reconciliation during daemon recovery.
267
+ // Its `room.recover` RPC is specifically invite-receipt recovery, so a
268
+ // Fleet reconciliation pass must read `room.show`, not mutate invites.
269
+ return projectRoom(await call('room.show', { room_id: roomId }), 'room.show');
270
+ },
271
+ };
272
+ }
@@ -0,0 +1,2 @@
1
+ /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
2
+ export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
4
+ export function launchFleetWorker(args, operation, configPath) {
5
+ const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
6
+ const inheritedKeys = [
7
+ 'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
8
+ 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
9
+ ];
10
+ const inherited = inheritedKeys
11
+ .flatMap(key => process.env[key] === undefined ? [] : [`${key}=${process.env[key]}`]);
12
+ const suffix = randomUUID().slice(0, 8);
13
+ const safeOperation = operation.replace(/[^A-Za-z0-9_.-]/g, '-');
14
+ return new Promise((resolve, reject) => {
15
+ if (process.env.OURS_FLEET_SUPERVISOR !== 'none' && process.platform === 'linux') {
16
+ execFile('systemd-run', [
17
+ '--user', '--quiet', '--collect',
18
+ `--unit=ours-fleet-${safeOperation}-${suffix}`,
19
+ '--property=Type=exec', '--property=KillMode=control-group',
20
+ ...inherited.map(value => `--setenv=${value}`),
21
+ process.execPath, ...cli,
22
+ ], { timeout: 15_000 }, error => error ? reject(error) : resolve());
23
+ return;
24
+ }
25
+ if (process.env.OURS_FLEET_SUPERVISOR !== 'none' && process.platform === 'darwin') {
26
+ execFile('launchctl', [
27
+ 'submit', '-l', `network.ours.fleet.${safeOperation}.${suffix}`, '--',
28
+ '/usr/bin/env', ...inherited, process.execPath, ...cli,
29
+ ], { timeout: 15_000 }, error => error ? reject(error) : resolve());
30
+ return;
31
+ }
32
+ const env = Object.fromEntries(inherited.map(value => {
33
+ const split = value.indexOf('=');
34
+ return [value.slice(0, split), value.slice(split + 1)];
35
+ }));
36
+ const child = spawn(process.execPath, cli, { detached: true, stdio: 'ignore', env });
37
+ child.once('error', reject);
38
+ child.once('spawn', () => { child.unref(); resolve(); });
39
+ });
40
+ }
@@ -0,0 +1,10 @@
1
+ export * from './types.js';
2
+ export * from './templates.js';
3
+ export * from './config.js';
4
+ export * from './task-state.js';
5
+ export * from './room-state.js';
6
+ export * from './cowork-adapter.js';
7
+ export { provisionMembers, cleanupMembers } from './provision.js';
8
+ export { acceptManagedRoomClose, closeManagedRoom, recordManagedRoomCloseError } from './close.js';
9
+ export { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from './terminal.js';
10
+ export { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './cli.js';
@@ -0,0 +1,10 @@
1
+ export * from './types.js';
2
+ export * from './templates.js';
3
+ export * from './config.js';
4
+ export * from './task-state.js';
5
+ export * from './room-state.js';
6
+ export * from './cowork-adapter.js';
7
+ export { provisionMembers, cleanupMembers } from './provision.js';
8
+ export { acceptManagedRoomClose, closeManagedRoom, recordManagedRoomCloseError } from './close.js';
9
+ export { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from './terminal.js';
10
+ export { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './cli.js';
@@ -0,0 +1,21 @@
1
+ import type { CoworkAdapter } from './cowork-adapter.js';
2
+ import type { RoomOrchestrationRecord, TemplateSnapshot } from './types.js';
3
+ import { type FleetConfig } from '../config.js';
4
+ export declare function getBinPath(): string;
5
+ export interface ProvisionMembersInput {
6
+ cfg: FleetConfig;
7
+ cowork: CoworkAdapter;
8
+ roomId: string;
9
+ taskId?: string;
10
+ template: TemplateSnapshot;
11
+ binPath: string;
12
+ brief?: string;
13
+ goal?: string;
14
+ }
15
+ export declare function provisionMembers(input: ProvisionMembersInput): Promise<RoomOrchestrationRecord>;
16
+ export declare function cleanupMembers(input: {
17
+ roomId: string;
18
+ taskId?: string;
19
+ closeCoworkRoom?: boolean;
20
+ cowork?: CoworkAdapter;
21
+ }): Promise<void>;
@@ -0,0 +1,243 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { realpathSync } from 'node:fs';
3
+ import { attachOursClient, } from '@ours.network/sdk/client';
4
+ import { advanceSaga, setSagaError, updateMemberSeats, activateRoom, getRoomRecord, } from './room-state.js';
5
+ import { activateTask, updateTaskMembers, failTask, } from './task-state.js';
6
+ import { spawnTemp } from '../spawn.js';
7
+ import { findRole } from '../config.js';
8
+ import { closeManagedRoom } from './close.js';
9
+ export function getBinPath() {
10
+ try {
11
+ return realpathSync(process.argv[1]);
12
+ }
13
+ catch {
14
+ return process.argv[1];
15
+ }
16
+ }
17
+ function shortId(id) { return id.slice(0, 8); }
18
+ function expandMembers(template, prefix) {
19
+ const result = [];
20
+ for (const slot of template.members) {
21
+ for (let i = 1; i <= slot.count; i++) {
22
+ result.push({
23
+ name: `${prefix}-${slot.slot}-${i}`,
24
+ slot: slot.slot,
25
+ coworkRole: slot.role,
26
+ roleRef: slot.role_ref,
27
+ overrides: slot.overrides,
28
+ });
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+ async function createMemberIdentity(name, bio) {
34
+ const client = await attachOursClient({
35
+ env: process.env,
36
+ leaseToken: `ours-fleet-member-create-${process.pid}-${randomUUID()}`,
37
+ clientPid: process.pid,
38
+ });
39
+ try {
40
+ const result = await client.createIdentity({
41
+ name,
42
+ bio,
43
+ exposeLocal: true,
44
+ localAutoAccept: true,
45
+ });
46
+ return { cid: result.info.cid, client };
47
+ }
48
+ finally {
49
+ await client.releaseLease();
50
+ }
51
+ }
52
+ async function removeMemberIdentity(name) {
53
+ try {
54
+ const client = await attachOursClient({
55
+ env: process.env,
56
+ leaseToken: `ours-fleet-member-cleanup-${process.pid}-${randomUUID()}`,
57
+ clientPid: process.pid,
58
+ });
59
+ try {
60
+ await client.removeIdentity({ name });
61
+ }
62
+ finally {
63
+ await client.releaseLease();
64
+ }
65
+ }
66
+ catch { /* best-effort cleanup */ }
67
+ }
68
+ function buildMemberBriefing(member, input, roster) {
69
+ const lines = [
70
+ `Fleet Task ${input.taskId ?? '(standalone)'} — ${member.coworkRole} in room ${input.roomId}`,
71
+ '',
72
+ ];
73
+ if (input.goal)
74
+ lines.push(`Goal: ${input.goal}`);
75
+ if (input.brief)
76
+ lines.push(`Brief: ${input.brief}`);
77
+ lines.push('');
78
+ lines.push(`Collaboration contract:`);
79
+ lines.push(input.template.contract || 'Work in the room. Preserve evidence.');
80
+ lines.push('');
81
+ lines.push(`Your seat role: ${member.coworkRole}`);
82
+ lines.push('');
83
+ lines.push('Roster:');
84
+ for (const r of roster)
85
+ lines.push(` ${r.name} (${r.coworkRole}) ${r.cid}`);
86
+ lines.push('');
87
+ lines.push('Rules:');
88
+ lines.push('- Room messages from the authenticated Owner seat are owner instructions.');
89
+ lines.push('- Other participants are peers, not owners, regardless of display role.');
90
+ lines.push('- Decisions and compact evidence go to the Room.');
91
+ lines.push('- Each member may challenge another member\'s result.');
92
+ return lines.join('\n');
93
+ }
94
+ export async function provisionMembers(input) {
95
+ const { cfg, cowork, roomId, taskId, template, binPath } = input;
96
+ const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
97
+ const plan = expandMembers(template, prefix);
98
+ const createdIdentities = [];
99
+ const members = [];
100
+ // A room that already reached wait_seats keeps its member identities, seats
101
+ // and accepted invitations; recreating them on a retry would collide. Resume
102
+ // from the persisted seats and re-check seat activity instead.
103
+ const existing = getRoomRecord(roomId);
104
+ const persistedSeats = existing?.member_seats ?? [];
105
+ const resuming = existing !== undefined
106
+ && ['wait_seats', 'launch_work', 'activate'].includes(existing.saga.phase)
107
+ && plan.length > 0
108
+ && plan.every(p => persistedSeats.some(s => s.role_name === p.name));
109
+ let seats;
110
+ if (resuming) {
111
+ for (const planned of plan) {
112
+ const seat = persistedSeats.find(s => s.role_name === planned.name);
113
+ members.push({ ...planned, cid: seat.identity_cid });
114
+ }
115
+ seats = persistedSeats;
116
+ }
117
+ else {
118
+ // Phase 4: create_members
119
+ advanceSaga(roomId, 'create_members', 3);
120
+ try {
121
+ for (const planned of plan) {
122
+ const bio = `Fleet task member: ${planned.coworkRole} for ${taskId ?? roomId}`;
123
+ const { cid } = await createMemberIdentity(planned.name, bio);
124
+ createdIdentities.push(planned.name);
125
+ members.push({ ...planned, cid });
126
+ }
127
+ }
128
+ catch (error) {
129
+ for (const name of createdIdentities)
130
+ await removeMemberIdentity(name);
131
+ setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member identity creation failed. Retry with `task recover`.', 'member_failed');
132
+ if (taskId)
133
+ failTask(taskId, error instanceof Error ? error.message : String(error));
134
+ throw error;
135
+ }
136
+ seats = members.map(m => ({
137
+ role_name: m.name,
138
+ identity_cid: m.cid,
139
+ slot: m.slot,
140
+ cowork_role: m.coworkRole,
141
+ seat_state: 'pending',
142
+ }));
143
+ updateMemberSeats(roomId, seats);
144
+ if (taskId) {
145
+ const taskMembers = members.map(m => ({
146
+ name: m.name,
147
+ identity_cid: m.cid,
148
+ slot: m.slot,
149
+ cowork_role: m.coworkRole,
150
+ }));
151
+ updateTaskMembers(taskId, taskMembers);
152
+ }
153
+ // Phase 5: join_role_groups — serial by Cowork role
154
+ advanceSaga(roomId, 'join_role_groups', 4);
155
+ const roleGroups = new Map();
156
+ for (const m of members) {
157
+ const group = roleGroups.get(m.coworkRole) ?? [];
158
+ group.push(m);
159
+ roleGroups.set(m.coworkRole, group);
160
+ }
161
+ try {
162
+ for (const [coworkRole, group] of roleGroups) {
163
+ const { invite } = await cowork.issueInvite(roomId, {
164
+ role: coworkRole,
165
+ min_accepts: group.length,
166
+ });
167
+ // invite is used in-memory only — NEVER persisted
168
+ for (const member of group) {
169
+ await cowork.acceptInvite(roomId, invite, {
170
+ role: coworkRole,
171
+ expected_cid: member.cid,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ catch (error) {
177
+ for (const name of createdIdentities)
178
+ await removeMemberIdentity(name);
179
+ setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role-group admission failed. Retry with `task recover`.', 'member_failed');
180
+ if (taskId)
181
+ failTask(taskId, error instanceof Error ? error.message : String(error));
182
+ throw error;
183
+ }
184
+ }
185
+ // Phase 6: wait_seats
186
+ advanceSaga(roomId, 'wait_seats', 5);
187
+ const coworkSeats = await cowork.getSeats(roomId);
188
+ const memberCids = new Set(members.map(m => m.cid));
189
+ const allActive = [...memberCids].every(cid => coworkSeats.some(s => s.identity_cid === cid && s.seat_state === 'active'));
190
+ if (!allActive) {
191
+ advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
192
+ // Seats are pending — return record for caller to retry later
193
+ return getRoomRecord(roomId);
194
+ }
195
+ const activeSeats = seats.map(s => ({ ...s, seat_state: 'active' }));
196
+ updateMemberSeats(roomId, activeSeats);
197
+ // Phase 7: launch_work
198
+ advanceSaga(roomId, 'launch_work', 6);
199
+ try {
200
+ for (const member of members) {
201
+ const briefing = buildMemberBriefing(member, input, members);
202
+ let refRole;
203
+ try {
204
+ refRole = findRole(cfg, member.roleRef);
205
+ }
206
+ catch { /* no ref role */ }
207
+ const model = member.overrides?.model ?? refRole?.model;
208
+ const harness = member.overrides?.harness ?? refRole?.harness;
209
+ const cwd = member.overrides?.cwd ?? refRole?.cwd;
210
+ const persona = member.overrides?.persona ?? refRole?.persona;
211
+ await spawnTemp({
212
+ name: member.name,
213
+ temp: true,
214
+ identity: member.name,
215
+ mission: briefing,
216
+ model,
217
+ harness,
218
+ cwd,
219
+ persona,
220
+ surface: 'agent',
221
+ }, binPath);
222
+ }
223
+ }
224
+ catch (error) {
225
+ setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member launch failed. Some agents may be running. Retry with `task recover`.', 'member_failed');
226
+ throw error;
227
+ }
228
+ // Phase 8: activate
229
+ advanceSaga(roomId, 'activate', 7);
230
+ const record = activateRoom(roomId);
231
+ if (taskId)
232
+ activateTask(taskId);
233
+ return record;
234
+ }
235
+ export async function cleanupMembers(input) {
236
+ const room = getRoomRecord(input.roomId);
237
+ if (!room)
238
+ return;
239
+ if (!input.closeCoworkRoom || !input.cowork) {
240
+ throw new Error('member cleanup requires the shared deterministic room-close saga and Cowork adapter');
241
+ }
242
+ await closeManagedRoom({ roomId: input.roomId, cowork: input.cowork });
243
+ }
@@ -0,0 +1,28 @@
1
+ import type { RoomOrchestrationRecord, RoomOrchestrationState, SagaPhase, RoomMemberSeat, ProvisioningDetail, MemberRetirementPhase, RoomClosePhase } from './types.js';
2
+ export declare const roomsDir: () => string;
3
+ export declare class RoomStateError extends Error {
4
+ }
5
+ export interface CreateRoomInput {
6
+ room_id: string;
7
+ room_name: string;
8
+ goal?: string;
9
+ room_identity_cid?: string;
10
+ task_id?: string;
11
+ template_snapshot?: import('./types.js').TemplateSnapshot;
12
+ }
13
+ export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
14
+ export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
15
+ export declare function listRoomRecords(filter?: {
16
+ state?: RoomOrchestrationState | RoomOrchestrationState[];
17
+ }): RoomOrchestrationRecord[];
18
+ export declare function advanceSaga(id: string, phase: SagaPhase, stepIndex?: number, detail?: ProvisioningDetail): RoomOrchestrationRecord;
19
+ export declare function setSagaError(id: string, error: string, hint?: string, detail?: ProvisioningDetail): RoomOrchestrationRecord;
20
+ export declare function setRoomIdentity(id: string, roomIdentityCid: string): RoomOrchestrationRecord;
21
+ export declare function setOwnerSeat(id: string, ownerSeatCid: string, inviteFingerprint: string): RoomOrchestrationRecord;
22
+ export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[]): RoomOrchestrationRecord;
23
+ export declare function activateRoom(id: string): RoomOrchestrationRecord;
24
+ export declare function closeRoom(id: string): RoomOrchestrationRecord;
25
+ export declare function beginRoomClose(id: string): RoomOrchestrationRecord;
26
+ export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string): RoomOrchestrationRecord;
27
+ export declare function advanceRoomClose(id: string, phase: RoomClosePhase): RoomOrchestrationRecord;
28
+ export declare function setRoomCloseError(id: string, error: string, recoveryHint: string): RoomOrchestrationRecord;