@ours.network/fleet 1.0.2 → 1.0.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.
Files changed (36) hide show
  1. package/README.md +4 -4
  2. package/dist/application/role-command-service.d.ts +29 -1
  3. package/dist/application/role-command-service.js +41 -2
  4. package/dist/application/role-creation-service.d.ts +32 -1
  5. package/dist/application/role-creation-service.js +56 -10
  6. package/dist/application/role-removal-service.d.ts +19 -0
  7. package/dist/application/role-removal-service.js +13 -3
  8. package/dist/application/session-mutations.d.ts +7 -0
  9. package/dist/application/session-mutations.js +8 -0
  10. package/dist/application/task-room-service.d.ts +227 -0
  11. package/dist/application/task-room-service.js +529 -0
  12. package/dist/build-info.json +4 -4
  13. package/dist/cli.js +39 -15
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +7 -6
  16. package/dist/harness/codex-app-server-proxy.d.ts +1 -1
  17. package/dist/harness/codex-app-server-proxy.js +23 -9
  18. package/dist/harness/types.d.ts +4 -2
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/owner-channel/channel.d.ts +5 -0
  22. package/dist/owner-channel/channel.js +143 -18
  23. package/dist/owner-channel/commands.d.ts +26 -1
  24. package/dist/owner-channel/commands.js +66 -128
  25. package/dist/rooms-tasks/cli.js +181 -462
  26. package/dist/rooms-tasks/provision.js +71 -8
  27. package/dist/rooms-tasks/types.d.ts +2 -0
  28. package/dist/runner.js +19 -44
  29. package/dist/session/acp.d.ts +8 -3
  30. package/dist/session/acp.js +24 -8
  31. package/dist/session/control.d.ts +10 -1
  32. package/dist/session/control.js +22 -21
  33. package/dist/watchdog/query.d.ts +2 -0
  34. package/dist/watchdog/query.js +7 -3
  35. package/dist/web/server.js +2 -2
  36. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { RoomStateError } from '../rooms-tasks/room-state.js';
4
4
  import { TaskStateError } from '../rooms-tasks/task-state.js';
5
5
  import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
6
6
  import { OWNER_COMMENT_LABEL, ownerNotices } from './notices.js';
7
+ import { TaskRoomApplicationError } from '../application/task-room-service.js';
7
8
  /** A malformed invocation; the dispatcher answers it with annotated help. */
8
9
  class OwnerCommandUsageError extends Error {
9
10
  }
@@ -53,6 +54,12 @@ function ownerTaskFailure(error) {
53
54
  };
54
55
  }
55
56
  function ownerRoomFailure(error) {
57
+ if (error instanceof TaskRoomApplicationError
58
+ && (error.code === 'room_not_found' || error.code === 'room_record_not_found'))
59
+ return {
60
+ kind: 'not_found', detail: `Room ${error.fields.room} was not found.`,
61
+ action: 'Run /room list to find a valid room ID.',
62
+ };
56
63
  if (error instanceof RoomStateError) {
57
64
  const missing = /^room not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
58
65
  if (missing)
@@ -228,9 +235,8 @@ export const ownerCommands = [
228
235
  name: 'tasks', usage: '/tasks [state]',
229
236
  summary: 'list tasks (optionally filter by state)',
230
237
  execute: async (ctx, args) => {
231
- const { listTasks } = await import('../rooms-tasks/task-state.js');
232
238
  const stateFilter = args?.trim() || undefined;
233
- const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
239
+ const tasks = ctx.listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
234
240
  await ctx.reply(renderMarkdownList({
235
241
  icon: '📋', title: 'Tasks', empty: 'No tasks found.',
236
242
  records: tasks.map(taskListRecord),
@@ -250,8 +256,7 @@ export const ownerCommands = [
250
256
  const rest = firstLineTokens.slice(1);
251
257
  const trailingLines = argLines.slice(1).join('\n').trim() || undefined;
252
258
  const showTask = async (id) => {
253
- const { getTask } = await import('../rooms-tasks/task-state.js');
254
- const t = getTask(id);
259
+ const { task: t } = ctx.getTask(id);
255
260
  await ctx.reply(renderMarkdownResult({
256
261
  icon: '📋', title: 'Task details',
257
262
  fields: [
@@ -278,14 +283,9 @@ export const ownerCommands = [
278
283
  const titleParts = rest.filter(r => r !== '--backlog' && !r.startsWith('--template=') && r !== '--no-room');
279
284
  if (!titleParts.length)
280
285
  throw new OwnerCommandUsageError('usage: /task create [--backlog] [--template=<name>|--no-room] <title>');
281
- const { createTask } = await import('../rooms-tasks/task-state.js');
282
- const t = createTask({
283
- title: titleParts.join(' '),
284
- origin: { type: 'owner_channel' },
285
- start: !backlog,
286
- ...(template ? { template: { name: template, version: 1, content_hash: '' } } : {}),
287
- ...(noRoom ? { no_room: true } : {}),
288
- ...(trailingLines ? { brief: trailingLines } : {}),
286
+ const t = await ctx.createTask({
287
+ title: titleParts.join(' '), origin: { type: 'owner_channel' },
288
+ backlog, template, noRoom, brief: trailingLines,
289
289
  });
290
290
  await ctx.reply(taskAction('Task created', t, [
291
291
  { label: 'Title', value: t.title },
@@ -295,7 +295,6 @@ export const ownerCommands = [
295
295
  break;
296
296
  }
297
297
  case 'list': {
298
- const { listTasks } = await import('../rooms-tasks/task-state.js');
299
298
  const filter = rest[0];
300
299
  const stateMap = {
301
300
  backlog: 'backlog', active: 'active', blocked: 'active',
@@ -304,7 +303,7 @@ export const ownerCommands = [
304
303
  const stateFilter = filter && stateMap[filter]
305
304
  ? { state: stateMap[filter] }
306
305
  : undefined;
307
- const tasks = listTasks(stateFilter);
306
+ const tasks = ctx.listTasks(stateFilter);
308
307
  if (filter === 'blocked') {
309
308
  const blocked = tasks.filter(t => t.blocked);
310
309
  await ctx.reply(renderMarkdownList({
@@ -328,33 +327,29 @@ export const ownerCommands = [
328
327
  case 'start': {
329
328
  if (!rest[0])
330
329
  throw new OwnerCommandUsageError('usage: /task start <id>');
331
- const { startTask } = await import('../rooms-tasks/task-state.js');
332
- const t = startTask(rest[0]);
330
+ const t = await ctx.startTask(rest[0]);
333
331
  await ctx.reply(taskAction('Task started', t));
334
332
  break;
335
333
  }
336
334
  case 'block': {
337
335
  if (!rest[0] || !rest[1])
338
336
  throw new OwnerCommandUsageError('usage: /task block <id> <reason>');
339
- const { blockTask } = await import('../rooms-tasks/task-state.js');
340
337
  const reason = rest.slice(1).join(' ');
341
- const t = blockTask(rest[0], reason);
338
+ const t = ctx.blockTask(rest[0], reason);
342
339
  await ctx.reply(taskAction('Task blocked', t, [{ label: 'Reason', value: reason }]));
343
340
  break;
344
341
  }
345
342
  case 'unblock': {
346
343
  if (!rest[0])
347
344
  throw new OwnerCommandUsageError('usage: /task unblock <id>');
348
- const { unblockTask } = await import('../rooms-tasks/task-state.js');
349
- const t = unblockTask(rest[0]);
345
+ const t = ctx.unblockTask(rest[0]);
350
346
  await ctx.reply(taskAction('Task unblocked', t));
351
347
  break;
352
348
  }
353
349
  case 'review': {
354
350
  if (!rest[0])
355
351
  throw new OwnerCommandUsageError('usage: /task review <id>');
356
- const { reviewTask } = await import('../rooms-tasks/task-state.js');
357
- const t = reviewTask(rest[0]);
352
+ const t = ctx.reviewTask(rest[0]);
358
353
  await ctx.reply(taskAction('Task ready for review', t));
359
354
  break;
360
355
  }
@@ -374,8 +369,7 @@ export const ownerCommands = [
374
369
  case 'delete': {
375
370
  if (rest.length !== 2 || rest[0] !== rest[1])
376
371
  throw new OwnerCommandUsageError('destructive: /task delete <id> <id> — provide the task ID twice');
377
- const { deleteTask } = await import('../rooms-tasks/task-state.js');
378
- const deleted = deleteTask(rest[0]);
372
+ const deleted = ctx.deleteTask(rest[0]);
379
373
  await ctx.reply(renderMarkdownResult({
380
374
  icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
381
375
  fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
@@ -385,40 +379,7 @@ export const ownerCommands = [
385
379
  case 'recover': {
386
380
  if (!rest[0])
387
381
  throw new OwnerCommandUsageError('usage: /task recover <id>');
388
- const { getTask } = await import('../rooms-tasks/task-state.js');
389
- const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
390
- const t = getTask(rest[0]);
391
- if (t.terminal_intent?.status === 'pending') {
392
- await ctx.terminalTask(t.task_id, t.terminal_intent.kind, t.terminal_intent.outcome);
393
- break;
394
- }
395
- const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
396
- const hints = [];
397
- if (t.state === 'provisioning' && room) {
398
- if (room.provisioning_detail === 'waiting_cowork')
399
- hints.push('Cowork socket unreachable');
400
- if (room.provisioning_detail === 'waiting_owner_invite')
401
- hints.push('Owner invite missing or invalid');
402
- if (room.provisioning_detail === 'owner_cid_mismatch')
403
- hints.push('Owner CID mismatch');
404
- if (room.provisioning_detail === 'member_failed')
405
- hints.push(`Member failed at step ${room.saga.step_index}`);
406
- }
407
- await ctx.reply(renderMarkdownResult({
408
- icon: '🛟', title: 'Task recovery',
409
- fields: [
410
- { label: 'Task', value: t.task_id, kind: 'code' },
411
- { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
412
- ...(room ? [
413
- { label: 'Room', value: room.room_id, kind: 'code' },
414
- { label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
415
- { label: 'Saga', value: room.saga.phase, kind: 'code' },
416
- ] : []),
417
- ],
418
- sections: hints.length
419
- ? [{ heading: 'Next steps', items: hints }]
420
- : [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
421
- }));
382
+ await ctx.recoverTask(rest[0]);
422
383
  break;
423
384
  }
424
385
  default:
@@ -442,11 +403,11 @@ export const ownerCommands = [
442
403
  {
443
404
  name: 'rooms', summary: 'list rooms',
444
405
  execute: noArgs('/rooms', async (ctx) => {
445
- const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
446
- const rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
406
+ const rooms = await ctx.listRoomQueries();
447
407
  await ctx.reply(renderMarkdownList({
448
408
  icon: '🏠', title: 'Rooms', empty: 'No rooms found.',
449
- records: rooms.map(roomListRecord),
409
+ records: rooms.map(room => `${roomStatus(room.state)} ${markdownCode(room.room_id)} — ${markdownProse(room.room_name)}`
410
+ + (room.orchestration?.task_id ? ` — Task ${markdownCode(room.orchestration.task_id)}` : '')),
450
411
  }));
451
412
  }),
452
413
  },
@@ -463,24 +424,28 @@ export const ownerCommands = [
463
424
  const rest = firstLineTokens.slice(1);
464
425
  const roomTrailingLines = argLines.slice(1).join('\n').trim() || undefined;
465
426
  const showRoom = async (id) => {
466
- const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
467
- const r = getRoomRecord(id);
468
- if (!r || r.state === 'closing' || r.state === 'closed')
469
- return ctx.reply(renderMarkdownFailure({
470
- kind: 'not_found', subject: `/room show ${id}`,
471
- detail: `Room ${id} was not found.`, action: 'Run /room list to find a valid room ID.',
472
- }));
427
+ let detail;
428
+ try {
429
+ detail = await ctx.getRoomQuery(id);
430
+ }
431
+ catch (error) {
432
+ if (!(error instanceof TaskRoomApplicationError && error.code === 'room_not_found'))
433
+ throw error;
434
+ return ctx.reply(renderMarkdownFailure({ kind: 'not_found', subject: `/room show ${id}`,
435
+ detail: `Room ${id} was not found.`, action: 'Run /room list to find a valid room ID.' }));
436
+ }
437
+ const { room, orchestration: r } = detail;
473
438
  await ctx.reply(renderMarkdownResult({
474
439
  icon: '🏠', title: 'Room details',
475
440
  fields: [
476
- { label: 'ID', value: r.room_id, kind: 'code' },
477
- { label: 'Name', value: r.room_name },
478
- { label: 'Status', value: roomStatus(r.state), kind: 'markdown' },
479
- { label: 'Saga', value: `${r.saga.phase} (step ${r.saga.step_index})` },
480
- ...(r.task_id ? [{ label: 'Task', value: r.task_id, kind: 'code' }] : []),
481
- ...(r.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
482
- ...(r.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
483
- { label: 'Created', value: r.created_at, kind: 'code' },
441
+ { label: 'ID', value: room.room_id, kind: 'code' },
442
+ { label: 'Name', value: room.room_name },
443
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
444
+ ...(r ? [{ label: 'Saga', value: `${r.saga.phase} (step ${r.saga.step_index})` }] : []),
445
+ ...(r?.task_id ? [{ label: 'Task', value: r.task_id, kind: 'code' }] : []),
446
+ ...(r?.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
447
+ ...(r?.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
448
+ ...(r ? [{ label: 'Created', value: r.created_at, kind: 'code' }] : []),
484
449
  ],
485
450
  }));
486
451
  };
@@ -492,22 +457,16 @@ export const ownerCommands = [
492
457
  const nameParts = rest.filter(r => !r.startsWith('--'));
493
458
  if (!nameParts.length)
494
459
  throw new OwnerCommandUsageError('usage: /room create --template=<name> <room name>');
495
- const { randomUUID } = await import('node:crypto');
496
- const { createRoomRecord } = await import('../rooms-tasks/room-state.js');
497
- const { resolveTemplate, snapshotTemplate } = await import('../rooms-tasks/templates.js');
498
- let templateSnapshot;
499
- if (templateName) {
500
- const tpl = resolveTemplate(templateName, {});
501
- if (!tpl)
460
+ let r;
461
+ try {
462
+ r = await ctx.createRoom({ name: nameParts.join(' '), template: templateName,
463
+ goal: roomTrailingLines });
464
+ }
465
+ catch (error) {
466
+ if (error instanceof TaskRoomApplicationError && error.code === 'template_not_found')
502
467
  throw new OwnerCommandUsageError(`unknown template: ${templateName}`);
503
- templateSnapshot = snapshotTemplate(tpl);
468
+ throw error;
504
469
  }
505
- const r = createRoomRecord({
506
- room_id: randomUUID().replace(/-/g, '').slice(0, 16),
507
- room_name: nameParts.join(' '),
508
- ...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
509
- ...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
510
- });
511
470
  await ctx.reply(roomAction('Room created', r, [
512
471
  { label: 'Name', value: r.room_name },
513
472
  ...(templateName ? [{ label: 'Template', value: templateName, kind: 'code' }] : []),
@@ -515,14 +474,14 @@ export const ownerCommands = [
515
474
  break;
516
475
  }
517
476
  case 'list': {
518
- const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
519
477
  const filter = rest[0];
520
- let rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
521
- if (filter === 'active')
522
- rooms = rooms.filter(r => r.state === 'active');
478
+ if (filter && !['active', 'provisioning', 'all'].includes(filter))
479
+ throw new OwnerCommandUsageError('usage: /room list [active|provisioning|all]');
480
+ const rooms = await ctx.listRoomQueries(filter === 'active' || filter === 'provisioning' ? { state: filter } : undefined);
523
481
  await ctx.reply(renderMarkdownList({
524
482
  icon: '🏠', title: 'Rooms', empty: 'No rooms found.',
525
- records: rooms.map(roomListRecord),
483
+ records: rooms.map(room => `${roomStatus(room.state)} ${markdownCode(room.room_id)} — ${markdownProse(room.room_name)}`
484
+ + (room.orchestration?.task_id ? ` — Task ${markdownCode(room.orchestration.task_id)}` : '')),
526
485
  }));
527
486
  break;
528
487
  }
@@ -542,32 +501,7 @@ export const ownerCommands = [
542
501
  case 'recover': {
543
502
  if (!rest[0])
544
503
  throw new OwnerCommandUsageError('usage: /room recover <id>');
545
- const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
546
- const r = getRoomRecord(rest[0]);
547
- if (!r) {
548
- await ctx.reply(renderMarkdownFailure({
549
- kind: 'not_found', subject: `/room recover ${rest[0]}`,
550
- detail: `Room ${rest[0]} was not found.`, action: 'Run /room list to find a valid room ID.',
551
- }));
552
- break;
553
- }
554
- if (r.state === 'closing' || r.state === 'closed') {
555
- await ctx.closeRoom(r.room_id);
556
- break;
557
- }
558
- await ctx.reply(renderMarkdownResult({
559
- icon: '🛟', title: 'Room recovery',
560
- fields: [
561
- { label: 'Room', value: r.room_id, kind: 'code' },
562
- { label: 'Status', value: roomStatus(r.state), kind: 'markdown' },
563
- { label: 'Saga', value: r.saga.phase, kind: 'code' },
564
- ...(r.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
565
- ...(r.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
566
- ],
567
- sections: [{ heading: 'Next step', items: [r.saga.error
568
- ? 'Run ours-fleet room recover from the CLI for full recovery.'
569
- : 'No recovery action is needed.'] }],
570
- }));
504
+ await ctx.recoverRoom(rest[0]);
571
505
  break;
572
506
  }
573
507
  default:
@@ -592,8 +526,7 @@ export const ownerCommands = [
592
526
  name: 'templates', aliases: ['template-list'],
593
527
  summary: 'list available room templates',
594
528
  execute: noArgs('/templates', async (ctx) => {
595
- const { listTemplates } = await import('../rooms-tasks/templates.js');
596
- const templates = listTemplates({});
529
+ const templates = ctx.listTemplateQueries();
597
530
  if (!templates.length)
598
531
  return ctx.reply('📐 No templates.');
599
532
  const lines = templates.map(t => {
@@ -613,10 +546,15 @@ export const ownerCommands = [
613
546
  const parts = args.trim().split(/\s+/);
614
547
  const sub = parts[0];
615
548
  const showTemplate = async (nameStr) => {
616
- const { resolveTemplate } = await import('../rooms-tasks/templates.js');
617
- const t = resolveTemplate(nameStr, {});
618
- if (!t)
549
+ let t;
550
+ try {
551
+ t = ctx.getTemplateQuery(nameStr);
552
+ }
553
+ catch (error) {
554
+ if (!(error instanceof TaskRoomApplicationError && error.code === 'template_not_found'))
555
+ throw error;
619
556
  return ctx.reply(`⚠️ template not found: ${nameStr}`);
557
+ }
620
558
  const lines = [
621
559
  `📐 Template: ${t.name}@${t.version}`,
622
560
  `Description: ${t.description}`,
@@ -635,8 +573,7 @@ export const ownerCommands = [
635
573
  break;
636
574
  }
637
575
  case 'list': {
638
- const { listTemplates } = await import('../rooms-tasks/templates.js');
639
- const templates = listTemplates({});
576
+ const templates = ctx.listTemplateQueries();
640
577
  if (!templates.length)
641
578
  return ctx.reply('📐 No templates.');
642
579
  const lines = templates.map(t => {
@@ -710,5 +647,6 @@ export function fleetCliOps(role, configPath) {
710
647
  }),
711
648
  closeRoom: roomId => launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath),
712
649
  settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
650
+ recoverTask: taskId => launchFleetWorker(['task', '_recover', taskId], `task-recover-${taskId}`, configPath),
713
651
  };
714
652
  }