@ours.network/fleet 0.19.0-nightly.2 → 0.19.0-nightly.4
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/dist/build-info.json +4 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +12 -1
- package/dist/doctor.js +125 -1
- package/dist/owner-channel/commands.js +324 -25
- package/dist/rooms-tasks/cli.js +309 -74
- package/dist/rooms-tasks/cowork-adapter.d.ts +21 -24
- package/dist/rooms-tasks/cowork-adapter.js +257 -33
- package/dist/rooms-tasks/index.d.ts +1 -0
- package/dist/rooms-tasks/index.js +1 -0
- package/dist/rooms-tasks/provision.d.ts +21 -0
- package/dist/rooms-tasks/provision.js +229 -0
- package/dist/rooms-tasks/room-state.d.ts +2 -0
- package/dist/rooms-tasks/room-state.js +2 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +1 -0
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { loadConfig } from '../config.js';
|
|
2
|
+
import { loadConfig, ConfigError } from '../config.js';
|
|
3
|
+
import { provisionMembers, cleanupMembers, getBinPath } from './provision.js';
|
|
3
4
|
import { resolveTemplate, listTemplates, snapshotTemplate, hashTemplate, } from './templates.js';
|
|
4
|
-
import { createTask, getTask, listTasks, startTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, } from './task-state.js';
|
|
5
|
-
import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, } from './room-state.js';
|
|
5
|
+
import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, updateTaskRoom, } from './task-state.js';
|
|
6
|
+
import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, } from './room-state.js';
|
|
7
|
+
import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
|
|
6
8
|
function die(e) {
|
|
7
9
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8
10
|
process.stderr.write(`error: ${msg}\n`);
|
|
@@ -14,6 +16,82 @@ function loadCfg(opts) {
|
|
|
14
16
|
function allTemplates(cfg) {
|
|
15
17
|
return cfg.roomTemplates ?? {};
|
|
16
18
|
}
|
|
19
|
+
function coworkFor(cfg) {
|
|
20
|
+
if (!cfg.rooms || cfg.rooms.provider !== 'cowork')
|
|
21
|
+
throw new ConfigError('rooms: configure provider: cowork before creating or querying rooms');
|
|
22
|
+
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
23
|
+
}
|
|
24
|
+
function resolveRoomTemplate(cfg, name) {
|
|
25
|
+
if (!name)
|
|
26
|
+
return undefined;
|
|
27
|
+
const template = resolveTemplate(name, allTemplates(cfg));
|
|
28
|
+
if (!template)
|
|
29
|
+
throw new Error(`template not found: ${name}`);
|
|
30
|
+
return snapshotTemplate(template);
|
|
31
|
+
}
|
|
32
|
+
async function provisionRoom(cfg, input) {
|
|
33
|
+
const rooms = cfg.rooms;
|
|
34
|
+
if (!rooms)
|
|
35
|
+
throw new ConfigError('rooms: configuration is required');
|
|
36
|
+
const attachOwner = rooms.defaults?.attach_owner !== false;
|
|
37
|
+
if (attachOwner && !cfg.ownerInvite)
|
|
38
|
+
throw new ConfigError('rooms.owner: public_invite or public_invite_file is required when attach_owner is enabled');
|
|
39
|
+
const cowork = coworkFor(cfg);
|
|
40
|
+
const goal = input.goal?.trim() || input.name;
|
|
41
|
+
const briefing = input.brief?.trim() || input.template?.contract?.trim() || goal;
|
|
42
|
+
const created = await cowork.createRoom({
|
|
43
|
+
room_name: input.name,
|
|
44
|
+
goal,
|
|
45
|
+
briefing,
|
|
46
|
+
quiet_membership: input.template?.room?.quiet_membership,
|
|
47
|
+
anonymous: input.template?.room?.anonymous,
|
|
48
|
+
});
|
|
49
|
+
let record = createRoomRecord({
|
|
50
|
+
room_id: created.room_id,
|
|
51
|
+
room_name: input.name,
|
|
52
|
+
room_identity_cid: created.identity_cid,
|
|
53
|
+
task_id: input.taskId,
|
|
54
|
+
template_snapshot: input.template,
|
|
55
|
+
});
|
|
56
|
+
input.onCreated?.(record);
|
|
57
|
+
record = advanceSaga(record.room_id, 'create_room', 1);
|
|
58
|
+
if (attachOwner) {
|
|
59
|
+
try {
|
|
60
|
+
record = advanceSaga(record.room_id, 'attach_owner', 2);
|
|
61
|
+
const accepted = await cowork.acceptInvite(record.room_id, cfg.ownerInvite, {
|
|
62
|
+
role: rooms.owner.role,
|
|
63
|
+
expected_cid: rooms.owner.expected_cid,
|
|
64
|
+
});
|
|
65
|
+
record = setOwnerSeat(record.room_id, accepted.seat_cid, cfg.ownerInviteFingerprint ?? '');
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
69
|
+
setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
|
|
70
|
+
? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
71
|
+
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
record = advanceSaga(record.room_id, 'create_members', 3);
|
|
76
|
+
if (input.template && input.template.members.length > 0) {
|
|
77
|
+
record = await provisionMembers({
|
|
78
|
+
cfg,
|
|
79
|
+
cowork,
|
|
80
|
+
roomId: record.room_id,
|
|
81
|
+
taskId: input.taskId,
|
|
82
|
+
template: input.template,
|
|
83
|
+
binPath: getBinPath(),
|
|
84
|
+
brief: input.brief,
|
|
85
|
+
goal: input.goal,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
record = activateRoom(record.room_id);
|
|
90
|
+
if (input.taskId)
|
|
91
|
+
activateTask(input.taskId);
|
|
92
|
+
}
|
|
93
|
+
return record;
|
|
94
|
+
}
|
|
17
95
|
export function registerTemplateCommands(parent, cOpt) {
|
|
18
96
|
const templateCmd = parent.command('template').description('room template operations');
|
|
19
97
|
cOpt(templateCmd.command('list'))
|
|
@@ -114,7 +192,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
114
192
|
.option('--no-room', 'create task without a room')
|
|
115
193
|
.option('--idempotency-key <key>', 'idempotency key')
|
|
116
194
|
.option('--json', 'JSON output')
|
|
117
|
-
.action((opts) => {
|
|
195
|
+
.action(async (opts) => {
|
|
118
196
|
try {
|
|
119
197
|
const cfg = loadCfg(opts);
|
|
120
198
|
let templateRef;
|
|
@@ -138,7 +216,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
138
216
|
brief = readFileSync(opts.briefFile, 'utf8');
|
|
139
217
|
}
|
|
140
218
|
const origin = { type: 'cli' };
|
|
141
|
-
|
|
219
|
+
let record = createTask({
|
|
142
220
|
title: opts.title,
|
|
143
221
|
brief,
|
|
144
222
|
brief_file: opts.briefFile,
|
|
@@ -147,6 +225,28 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
147
225
|
idempotency_key: opts.idempotencyKey,
|
|
148
226
|
start: !opts.backlog,
|
|
149
227
|
});
|
|
228
|
+
if (!opts.backlog && opts.room !== false && templateRef && !record.room_id) {
|
|
229
|
+
const template = resolveRoomTemplate(cfg, templateRef.name);
|
|
230
|
+
try {
|
|
231
|
+
await provisionRoom(cfg, {
|
|
232
|
+
name: opts.title,
|
|
233
|
+
goal: opts.title,
|
|
234
|
+
brief,
|
|
235
|
+
template,
|
|
236
|
+
taskId: record.task_id,
|
|
237
|
+
onCreated: room => {
|
|
238
|
+
record = updateTaskRoom(record.task_id, room.room_id, room.room_identity_cid);
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
record = getTask(record.task_id);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (error instanceof CoworkUnavailableError) {
|
|
245
|
+
blockTask(record.task_id, 'Cowork management socket is unavailable');
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
150
250
|
if (opts.json) {
|
|
151
251
|
console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
|
|
152
252
|
return;
|
|
@@ -224,12 +324,29 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
224
324
|
die(e);
|
|
225
325
|
}
|
|
226
326
|
});
|
|
227
|
-
taskCmd.command('start <id>')
|
|
327
|
+
cOpt(taskCmd.command('start <id>'))
|
|
228
328
|
.description('start a backlog task')
|
|
229
329
|
.option('--json', 'JSON output')
|
|
230
|
-
.action((id, opts) => {
|
|
330
|
+
.action(async (id, opts) => {
|
|
231
331
|
try {
|
|
232
|
-
const
|
|
332
|
+
const cfg = loadCfg(opts);
|
|
333
|
+
let t = startTask(id);
|
|
334
|
+
if (t.template && !t.room_id) {
|
|
335
|
+
const template = resolveRoomTemplate(cfg, t.template.name);
|
|
336
|
+
if (!template || template.content_hash !== t.template.content_hash)
|
|
337
|
+
throw new Error(`task template snapshot no longer matches ${t.template.name}@${t.template.version}`);
|
|
338
|
+
await provisionRoom(cfg, {
|
|
339
|
+
name: t.title,
|
|
340
|
+
goal: t.title,
|
|
341
|
+
brief: t.brief,
|
|
342
|
+
template,
|
|
343
|
+
taskId: t.task_id,
|
|
344
|
+
onCreated: room => {
|
|
345
|
+
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
t = getTask(t.task_id);
|
|
349
|
+
}
|
|
233
350
|
if (opts.json) {
|
|
234
351
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
235
352
|
return;
|
|
@@ -289,18 +406,32 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
289
406
|
die(e);
|
|
290
407
|
}
|
|
291
408
|
});
|
|
292
|
-
taskCmd.command('done <id>')
|
|
409
|
+
cOpt(taskCmd.command('done <id>'))
|
|
293
410
|
.description('complete a task')
|
|
294
411
|
.option('--summary <text>', 'completion summary')
|
|
295
412
|
.option('--summary-file <path>', 'completion summary from file')
|
|
296
413
|
.option('--json', 'JSON output')
|
|
297
|
-
.action((id, opts) => {
|
|
414
|
+
.action(async (id, opts) => {
|
|
298
415
|
try {
|
|
299
416
|
let summary = opts.summary;
|
|
300
417
|
if (opts.summaryFile)
|
|
301
418
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
302
419
|
const outcome = summary ? { summary } : undefined;
|
|
303
420
|
const t = completeTask(id, outcome);
|
|
421
|
+
if (t.room_id) {
|
|
422
|
+
const cfg = loadCfg(opts);
|
|
423
|
+
const shouldClose = cfg.tasks?.close_room_on_done
|
|
424
|
+
?? cfg.rooms?.defaults?.close_when_task_done
|
|
425
|
+
?? false;
|
|
426
|
+
if (shouldClose) {
|
|
427
|
+
try {
|
|
428
|
+
await coworkFor(cfg).closeRoom(t.room_id);
|
|
429
|
+
closeRoomRecord(t.room_id);
|
|
430
|
+
}
|
|
431
|
+
catch { /* room close is best-effort on task done */ }
|
|
432
|
+
await cleanupMembers({ roomId: t.room_id, taskId: id }).catch(() => { });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
304
435
|
if (opts.json) {
|
|
305
436
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
306
437
|
return;
|
|
@@ -311,14 +442,23 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
311
442
|
die(e);
|
|
312
443
|
}
|
|
313
444
|
});
|
|
314
|
-
taskCmd.command('cancel <id> <confirm-id>')
|
|
445
|
+
cOpt(taskCmd.command('cancel <id> <confirm-id>'))
|
|
315
446
|
.description('cancel a task (requires ID twice for confirmation)')
|
|
316
447
|
.option('--json', 'JSON output')
|
|
317
|
-
.action((id, confirmId, opts) => {
|
|
448
|
+
.action(async (id, confirmId, opts) => {
|
|
318
449
|
try {
|
|
319
450
|
if (id !== confirmId)
|
|
320
451
|
die(new Error('confirmation ID must match task ID'));
|
|
321
452
|
const t = cancelTask(id);
|
|
453
|
+
if (t.room_id) {
|
|
454
|
+
const cfg = loadCfg(opts);
|
|
455
|
+
await cleanupMembers({
|
|
456
|
+
roomId: t.room_id,
|
|
457
|
+
taskId: id,
|
|
458
|
+
closeCoworkRoom: true,
|
|
459
|
+
cowork: coworkFor(cfg),
|
|
460
|
+
}).catch(() => { });
|
|
461
|
+
}
|
|
322
462
|
if (opts.json) {
|
|
323
463
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
324
464
|
return;
|
|
@@ -329,11 +469,12 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
329
469
|
die(e);
|
|
330
470
|
}
|
|
331
471
|
});
|
|
332
|
-
taskCmd.command('recover <id>')
|
|
472
|
+
cOpt(taskCmd.command('recover <id>'))
|
|
333
473
|
.description('attempt to recover a stuck task')
|
|
334
474
|
.option('--json', 'JSON output')
|
|
335
|
-
.action((id, opts) => {
|
|
475
|
+
.action(async (id, opts) => {
|
|
336
476
|
try {
|
|
477
|
+
const cfg = loadCfg(opts);
|
|
337
478
|
const t = getTask(id);
|
|
338
479
|
const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
339
480
|
const result = {
|
|
@@ -350,6 +491,28 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
350
491
|
result.recovery_actions.push('Owner CID mismatch — verify rooms.owner.expected_cid matches Messenger identity');
|
|
351
492
|
if (room.provisioning_detail === 'member_failed')
|
|
352
493
|
result.recovery_actions.push(`Member creation failed at saga step ${room.saga.step_index} — inspect and retry`);
|
|
494
|
+
const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
|
|
495
|
+
if (resumable.includes(room.saga.phase)) {
|
|
496
|
+
const template = t.template ? resolveRoomTemplate(cfg, t.template.name) : undefined;
|
|
497
|
+
if (template) {
|
|
498
|
+
try {
|
|
499
|
+
await provisionMembers({
|
|
500
|
+
cfg,
|
|
501
|
+
cowork: coworkFor(cfg),
|
|
502
|
+
roomId: room.room_id,
|
|
503
|
+
taskId: t.task_id,
|
|
504
|
+
template,
|
|
505
|
+
binPath: getBinPath(),
|
|
506
|
+
brief: t.brief,
|
|
507
|
+
goal: t.title,
|
|
508
|
+
});
|
|
509
|
+
result.recovery_actions.push('Provisioning resumed successfully');
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
result.recovery_actions.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
353
516
|
}
|
|
354
517
|
if (opts.json) {
|
|
355
518
|
console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
|
|
@@ -382,13 +545,12 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
382
545
|
.option('--brief <text>', 'room briefing')
|
|
383
546
|
.option('--brief-file <path>', 'room briefing from file')
|
|
384
547
|
.option('--json', 'JSON output')
|
|
385
|
-
.action((opts) => {
|
|
548
|
+
.action(async (opts) => {
|
|
386
549
|
try {
|
|
387
550
|
const cfg = loadCfg(opts);
|
|
388
551
|
let brief = opts.brief;
|
|
389
552
|
if (opts.briefFile)
|
|
390
553
|
brief = readFileSync(opts.briefFile, 'utf8');
|
|
391
|
-
const roomId = `room-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
392
554
|
let templateSnapshot;
|
|
393
555
|
if (opts.template) {
|
|
394
556
|
const t = resolveTemplate(opts.template, allTemplates(cfg));
|
|
@@ -396,10 +558,11 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
396
558
|
die(new Error(`template not found: ${opts.template}`));
|
|
397
559
|
templateSnapshot = snapshotTemplate(t);
|
|
398
560
|
}
|
|
399
|
-
const record =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
561
|
+
const record = await provisionRoom(cfg, {
|
|
562
|
+
name: opts.name,
|
|
563
|
+
goal: opts.goal,
|
|
564
|
+
brief,
|
|
565
|
+
template: templateSnapshot,
|
|
403
566
|
});
|
|
404
567
|
if (opts.json) {
|
|
405
568
|
console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
|
|
@@ -413,17 +576,22 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
413
576
|
die(e);
|
|
414
577
|
}
|
|
415
578
|
});
|
|
416
|
-
roomCmd.command('list')
|
|
579
|
+
cOpt(roomCmd.command('list'))
|
|
417
580
|
.description('list rooms')
|
|
418
581
|
.option('--state <state>', 'filter by state (active|provisioning|closing|closed|all)')
|
|
419
582
|
.option('--json', 'JSON output')
|
|
420
|
-
.action((opts) => {
|
|
583
|
+
.action(async (opts) => {
|
|
421
584
|
try {
|
|
585
|
+
const cfg = loadCfg(opts);
|
|
422
586
|
let stateFilter;
|
|
423
587
|
if (opts.state && opts.state !== 'all') {
|
|
424
588
|
stateFilter = opts.state;
|
|
425
589
|
}
|
|
426
|
-
const
|
|
590
|
+
const local = new Map(listRoomRecords().map(room => [room.room_id, room]));
|
|
591
|
+
const coworkRooms = await coworkFor(cfg).listRooms();
|
|
592
|
+
const rooms = coworkRooms
|
|
593
|
+
.filter(room => !stateFilter || room.state === stateFilter)
|
|
594
|
+
.map(room => ({ ...room, orchestration: local.get(room.room_id) ?? null }));
|
|
427
595
|
if (opts.json) {
|
|
428
596
|
console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
|
|
429
597
|
return;
|
|
@@ -433,122 +601,189 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
433
601
|
return;
|
|
434
602
|
}
|
|
435
603
|
for (const r of rooms)
|
|
436
|
-
console.log(`${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
604
|
+
console.log(`${r.room_id} ${r.state} ${r.room_name}${r.orchestration?.task_id ? ` (task: ${r.orchestration.task_id})` : ''}`);
|
|
437
605
|
}
|
|
438
606
|
catch (e) {
|
|
439
607
|
die(e);
|
|
440
608
|
}
|
|
441
609
|
});
|
|
442
|
-
roomCmd.command('show <id>')
|
|
610
|
+
cOpt(roomCmd.command('show <id>'))
|
|
443
611
|
.description('show room details')
|
|
444
612
|
.option('--json', 'JSON output')
|
|
445
|
-
.action((id, opts) => {
|
|
613
|
+
.action(async (id, opts) => {
|
|
446
614
|
try {
|
|
447
|
-
const
|
|
448
|
-
|
|
615
|
+
const cfg = loadCfg(opts);
|
|
616
|
+
const cowork = await coworkFor(cfg).getRoom(id);
|
|
617
|
+
if (!cowork)
|
|
449
618
|
die(new Error(`room not found: ${id}`));
|
|
619
|
+
const r = getRoomRecord(id);
|
|
450
620
|
if (opts.json) {
|
|
451
|
-
console.log(JSON.stringify({ schema_version: 1, room: r }, null, 2));
|
|
621
|
+
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
|
|
452
622
|
return;
|
|
453
623
|
}
|
|
454
|
-
console.log(`Room: ${
|
|
455
|
-
console.log(`Name: ${
|
|
456
|
-
console.log(`State: ${
|
|
457
|
-
|
|
624
|
+
console.log(`Room: ${cowork.room_id}`);
|
|
625
|
+
console.log(`Name: ${cowork.room_name}`);
|
|
626
|
+
console.log(`State: ${cowork.state}`);
|
|
627
|
+
console.log(`Identity CID: ${cowork.identity_cid}`);
|
|
628
|
+
if (r?.task_id)
|
|
458
629
|
console.log(`Task: ${r.task_id}`);
|
|
459
|
-
if (r
|
|
460
|
-
console.log(`
|
|
461
|
-
|
|
462
|
-
if (r.provisioning_detail)
|
|
630
|
+
if (r)
|
|
631
|
+
console.log(`Saga: ${r.saga.phase} (step ${r.saga.step_index})`);
|
|
632
|
+
if (r?.provisioning_detail)
|
|
463
633
|
console.log(`Detail: ${r.provisioning_detail}`);
|
|
464
|
-
if (r
|
|
634
|
+
if (r?.saga.error)
|
|
465
635
|
console.log(`Error: ${r.saga.error}`);
|
|
466
|
-
if (
|
|
636
|
+
if (cowork.seats.length) {
|
|
467
637
|
console.log('Members:');
|
|
468
|
-
for (const s of
|
|
469
|
-
console.log(` ${s.
|
|
638
|
+
for (const s of cowork.seats)
|
|
639
|
+
console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
|
|
640
|
+
}
|
|
641
|
+
if (r)
|
|
642
|
+
console.log(`Tracked by Fleet since: ${r.created_at}`);
|
|
643
|
+
}
|
|
644
|
+
catch (e) {
|
|
645
|
+
die(e);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
cOpt(roomCmd.command('open <id>'))
|
|
649
|
+
.description('open room in Cowork local console')
|
|
650
|
+
.option('--json', 'JSON output')
|
|
651
|
+
.action(async (id, opts) => {
|
|
652
|
+
try {
|
|
653
|
+
const cfg = loadCfg(opts);
|
|
654
|
+
const room = await coworkFor(cfg).getRoom(id);
|
|
655
|
+
if (!room)
|
|
656
|
+
die(new Error(`room not found: ${id}`));
|
|
657
|
+
const url = `http://localhost:4460/room/${id}`;
|
|
658
|
+
if (opts.json) {
|
|
659
|
+
console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
|
|
660
|
+
return;
|
|
470
661
|
}
|
|
471
|
-
console.log(`
|
|
472
|
-
|
|
473
|
-
console.log(`Activated: ${r.activated_at}`);
|
|
474
|
-
if (r.closed_at)
|
|
475
|
-
console.log(`Closed: ${r.closed_at}`);
|
|
662
|
+
console.log(`Room ${id} — ${room.room_name}`);
|
|
663
|
+
console.log(`Local console: ${url}`);
|
|
476
664
|
}
|
|
477
665
|
catch (e) {
|
|
478
666
|
die(e);
|
|
479
667
|
}
|
|
480
668
|
});
|
|
481
|
-
roomCmd.command('members <id>')
|
|
669
|
+
cOpt(roomCmd.command('members <id>'))
|
|
482
670
|
.description('show room members')
|
|
483
671
|
.option('--json', 'JSON output')
|
|
484
|
-
.action((id, opts) => {
|
|
672
|
+
.action(async (id, opts) => {
|
|
485
673
|
try {
|
|
674
|
+
const cfg = loadCfg(opts);
|
|
486
675
|
const r = getRoomRecord(id);
|
|
487
|
-
|
|
488
|
-
die(new Error(`room not found: ${id}`));
|
|
676
|
+
const members = await coworkFor(cfg).getSeats(id);
|
|
489
677
|
if (opts.json) {
|
|
490
678
|
console.log(JSON.stringify({
|
|
491
679
|
schema_version: 1,
|
|
492
|
-
room_id:
|
|
493
|
-
members
|
|
494
|
-
owner_seat_cid: r
|
|
680
|
+
room_id: id,
|
|
681
|
+
members,
|
|
682
|
+
owner_seat_cid: r?.owner_seat_cid ?? null,
|
|
495
683
|
}, null, 2));
|
|
496
684
|
return;
|
|
497
685
|
}
|
|
498
|
-
console.log(`Room ${r
|
|
499
|
-
if (r
|
|
686
|
+
console.log(`Room ${id}${r ? ` — ${r.room_name}` : ''}`);
|
|
687
|
+
if (r?.owner_seat_cid)
|
|
500
688
|
console.log(`Owner: ${r.owner_seat_cid}`);
|
|
501
|
-
if (!
|
|
689
|
+
if (!members.length) {
|
|
502
690
|
console.log('No members.');
|
|
503
691
|
return;
|
|
504
692
|
}
|
|
505
|
-
for (const s of
|
|
506
|
-
console.log(` ${s.
|
|
693
|
+
for (const s of members)
|
|
694
|
+
console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
|
|
507
695
|
}
|
|
508
696
|
catch (e) {
|
|
509
697
|
die(e);
|
|
510
698
|
}
|
|
511
699
|
});
|
|
512
|
-
roomCmd.command('close <id> <confirm-id>')
|
|
700
|
+
cOpt(roomCmd.command('close <id> <confirm-id>'))
|
|
513
701
|
.description('close a room (requires ID twice for confirmation)')
|
|
514
702
|
.option('--json', 'JSON output')
|
|
515
|
-
.action((id, confirmId, opts) => {
|
|
703
|
+
.action(async (id, confirmId, opts) => {
|
|
516
704
|
try {
|
|
517
705
|
if (id !== confirmId)
|
|
518
706
|
die(new Error('confirmation ID must match room ID'));
|
|
519
|
-
const
|
|
707
|
+
const cfg = loadCfg(opts);
|
|
708
|
+
await coworkFor(cfg).closeRoom(id);
|
|
709
|
+
const existing = getRoomRecord(id);
|
|
710
|
+
const r = existing ? closeRoomRecord(id) : undefined;
|
|
520
711
|
if (opts.json) {
|
|
521
|
-
console.log(JSON.stringify({ schema_version: 1,
|
|
712
|
+
console.log(JSON.stringify({ schema_version: 1, room_id: id, state: 'closed', orchestration: r ?? null }, null, 2));
|
|
522
713
|
return;
|
|
523
714
|
}
|
|
524
|
-
console.log(`Room ${
|
|
715
|
+
console.log(`Room ${id} · closed`);
|
|
525
716
|
}
|
|
526
717
|
catch (e) {
|
|
527
718
|
die(e);
|
|
528
719
|
}
|
|
529
720
|
});
|
|
530
|
-
roomCmd.command('recover <id>')
|
|
721
|
+
cOpt(roomCmd.command('recover <id>'))
|
|
531
722
|
.description('attempt to recover a stuck room')
|
|
532
723
|
.option('--json', 'JSON output')
|
|
533
|
-
.action((id, opts) => {
|
|
724
|
+
.action(async (id, opts) => {
|
|
534
725
|
try {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
726
|
+
const cfg = loadCfg(opts);
|
|
727
|
+
const adapter = coworkFor(cfg);
|
|
728
|
+
const cowork = await adapter.recoverRoom(id);
|
|
729
|
+
let r = getRoomRecord(id);
|
|
730
|
+
if (r && !r.owner_seat_cid
|
|
731
|
+
&& (r.provisioning_detail === 'waiting_owner_invite'
|
|
732
|
+
|| r.provisioning_detail === 'owner_cid_mismatch')) {
|
|
733
|
+
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
734
|
+
const existing = (await adapter.getSeats(id))
|
|
735
|
+
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
736
|
+
if (!existing && !cfg.ownerInvite)
|
|
737
|
+
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
738
|
+
let acceptedCid = existing?.identity_cid;
|
|
739
|
+
if (!acceptedCid) {
|
|
740
|
+
const accepted = await adapter.acceptInvite(id, cfg.ownerInvite, {
|
|
741
|
+
role: cfg.rooms.owner.role,
|
|
742
|
+
expected_cid: cfg.rooms.owner.expected_cid,
|
|
743
|
+
});
|
|
744
|
+
acceptedCid = accepted.seat_cid;
|
|
745
|
+
}
|
|
746
|
+
setOwnerSeat(id, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
747
|
+
r = advanceSaga(id, 'create_members', 3);
|
|
748
|
+
}
|
|
538
749
|
const actions = [];
|
|
539
|
-
if (r
|
|
750
|
+
if (r?.saga.error)
|
|
540
751
|
actions.push(`Last error: ${r.saga.error}`);
|
|
541
|
-
if (r
|
|
752
|
+
if (r?.saga.recovery_hint)
|
|
542
753
|
actions.push(r.saga.recovery_hint);
|
|
543
|
-
if (r
|
|
754
|
+
if (r?.provisioning_detail === 'waiting_cowork')
|
|
544
755
|
actions.push('Check ours-cowork service status');
|
|
545
|
-
if (r
|
|
756
|
+
if (r?.provisioning_detail === 'waiting_owner_invite')
|
|
546
757
|
actions.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
758
|
+
if (r && r.state === 'provisioning') {
|
|
759
|
+
const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
|
|
760
|
+
if (resumable.includes(r.saga.phase) && r.template_snapshot) {
|
|
761
|
+
try {
|
|
762
|
+
const template = resolveRoomTemplate(cfg, r.template_snapshot.name);
|
|
763
|
+
if (template) {
|
|
764
|
+
await provisionMembers({
|
|
765
|
+
cfg,
|
|
766
|
+
cowork: adapter,
|
|
767
|
+
roomId: r.room_id,
|
|
768
|
+
taskId: r.task_id,
|
|
769
|
+
template,
|
|
770
|
+
binPath: getBinPath(),
|
|
771
|
+
goal: r.room_name,
|
|
772
|
+
});
|
|
773
|
+
r = getRoomRecord(id);
|
|
774
|
+
actions.push('Provisioning resumed successfully');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
actions.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
547
782
|
if (opts.json) {
|
|
548
|
-
console.log(JSON.stringify({ schema_version: 1, room: r, recovery_actions: actions }, null, 2));
|
|
783
|
+
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
|
|
549
784
|
return;
|
|
550
785
|
}
|
|
551
|
-
console.log(`Room ${
|
|
786
|
+
console.log(`Room ${cowork.room_id} · ${cowork.state}${r ? ` · saga: ${r.saga.phase}` : ''}`);
|
|
552
787
|
if (actions.length) {
|
|
553
788
|
console.log('Recovery:');
|
|
554
789
|
for (const a of actions)
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Typed client for the ours-cowork v1 private management-socket protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* JSON RPC. All methods fail closed: if Cowork is unreachable or returns
|
|
7
|
-
* an unexpected response, the operation fails rather than guessing.
|
|
8
|
-
*
|
|
9
|
-
* BLOCKED: The management socket protocol is defined in ours-cowork but
|
|
10
|
-
* the exact prerelease wire format needs verification. This adapter
|
|
11
|
-
* defines the interface Fleet needs and provides a fail-closed stub
|
|
12
|
-
* that reports the blocker. Replace the stub with socket calls once
|
|
13
|
-
* the Cowork management socket contract is verified.
|
|
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.
|
|
14
6
|
*/
|
|
7
|
+
import type { Socket } from 'node:net';
|
|
15
8
|
export interface CoworkRoomCreateResult {
|
|
16
9
|
room_id: string;
|
|
17
10
|
identity_name: string;
|
|
@@ -44,8 +37,8 @@ export interface CoworkAdapter {
|
|
|
44
37
|
available(): Promise<boolean>;
|
|
45
38
|
createRoom(opts: {
|
|
46
39
|
room_name: string;
|
|
47
|
-
goal
|
|
48
|
-
briefing
|
|
40
|
+
goal: string;
|
|
41
|
+
briefing: string;
|
|
49
42
|
quiet_membership?: boolean;
|
|
50
43
|
anonymous?: boolean;
|
|
51
44
|
}): Promise<CoworkRoomCreateResult>;
|
|
@@ -57,24 +50,28 @@ export interface CoworkAdapter {
|
|
|
57
50
|
role: string;
|
|
58
51
|
min_accepts: number;
|
|
59
52
|
}): Promise<CoworkInviteResult>;
|
|
60
|
-
acceptAsIdentity(roomId: string, invite: string, identityCid: string, opts: {
|
|
61
|
-
role: string;
|
|
62
|
-
}): Promise<CoworkInviteAcceptResult>;
|
|
63
53
|
getRoom(roomId: string): Promise<CoworkRoomInfo | undefined>;
|
|
64
54
|
listRooms(): Promise<CoworkRoomInfo[]>;
|
|
65
55
|
closeRoom(roomId: string): Promise<void>;
|
|
66
56
|
getSeats(roomId: string): Promise<CoworkSeatInfo[]>;
|
|
57
|
+
recoverRoom(roomId: string): Promise<CoworkRoomInfo>;
|
|
67
58
|
}
|
|
68
59
|
export declare class CoworkUnavailableError extends Error {
|
|
69
|
-
constructor(message?: string);
|
|
60
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
70
61
|
}
|
|
71
62
|
export declare class CoworkProtocolError extends Error {
|
|
72
63
|
readonly operation: string;
|
|
73
|
-
|
|
64
|
+
readonly code: string;
|
|
65
|
+
constructor(operation: string, message: string, code?: string, options?: ErrorOptions);
|
|
74
66
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
export interface CoworkAdapterOptions {
|
|
68
|
+
configPath?: string;
|
|
69
|
+
socketPath?: string;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
env?: NodeJS.ProcessEnv;
|
|
72
|
+
home?: string;
|
|
73
|
+
connect?: (path: string) => Socket;
|
|
74
|
+
}
|
|
75
|
+
/** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
|
|
76
|
+
export declare function resolveCoworkSocketPath(options?: CoworkAdapterOptions): string;
|
|
77
|
+
export declare function createCoworkAdapter(options?: CoworkAdapterOptions): CoworkAdapter;
|