@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.
@@ -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
- const record = createTask({
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 t = startTask(id);
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 = createRoomRecord({
400
- room_id: roomId,
401
- room_name: opts.name,
402
- template_snapshot: templateSnapshot,
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 rooms = listRoomRecords(stateFilter ? { state: stateFilter } : undefined);
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 r = getRoomRecord(id);
448
- if (!r)
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: ${r.room_id}`);
455
- console.log(`Name: ${r.room_name}`);
456
- console.log(`State: ${r.state}`);
457
- if (r.task_id)
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.room_identity_cid)
460
- console.log(`Identity CID: ${r.room_identity_cid}`);
461
- console.log(`Saga: ${r.saga.phase} (step ${r.saga.step_index})`);
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.saga.error)
634
+ if (r?.saga.error)
465
635
  console.log(`Error: ${r.saga.error}`);
466
- if (r.member_seats.length) {
636
+ if (cowork.seats.length) {
467
637
  console.log('Members:');
468
- for (const s of r.member_seats)
469
- console.log(` ${s.role_name} (${s.cowork_role}) ${s.seat_state}`);
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(`Created: ${r.created_at}`);
472
- if (r.activated_at)
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
- if (!r)
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: r.room_id,
493
- members: r.member_seats,
494
- owner_seat_cid: r.owner_seat_cid ?? null,
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.room_id} — ${r.room_name}`);
499
- if (r.owner_seat_cid)
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 (!r.member_seats.length) {
689
+ if (!members.length) {
502
690
  console.log('No members.');
503
691
  return;
504
692
  }
505
- for (const s of r.member_seats)
506
- console.log(` ${s.role_name} (${s.cowork_role}) ${s.seat_state} ${s.identity_cid}`);
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 r = closeRoomRecord(id);
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, room: r }, null, 2));
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 ${r.room_id} · closed`);
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 r = getRoomRecord(id);
536
- if (!r)
537
- die(new Error(`room not found: ${id}`));
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.saga.error)
750
+ if (r?.saga.error)
540
751
  actions.push(`Last error: ${r.saga.error}`);
541
- if (r.saga.recovery_hint)
752
+ if (r?.saga.recovery_hint)
542
753
  actions.push(r.saga.recovery_hint);
543
- if (r.provisioning_detail === 'waiting_cowork')
754
+ if (r?.provisioning_detail === 'waiting_cowork')
544
755
  actions.push('Check ours-cowork service status');
545
- if (r.provisioning_detail === 'waiting_owner_invite')
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 ${r.room_id} · ${r.state} · saga: ${r.saga.phase}`);
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
- * Cowork private management-socket adapter.
2
+ * Typed client for the ours-cowork v1 private management-socket protocol.
3
3
  *
4
- * Fleet orchestrates tasks/rooms but Cowork owns room state. This adapter
5
- * communicates with Cowork via its Unix management socket using versioned
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?: string;
48
- briefing?: string;
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
- constructor(operation: string, message: string);
64
+ readonly code: string;
65
+ constructor(operation: string, message: string, code?: string, options?: ErrorOptions);
74
66
  }
75
- /**
76
- * Fail-closed stub adapter. Every method throws CoworkUnavailableError.
77
- * This is the correct behavior until the Cowork management socket
78
- * integration is verified against the prerelease wire format.
79
- */
80
- export declare function createStubAdapter(): CoworkAdapter;
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;