@ours.network/fleet 1.0.3 → 1.0.5

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 (43) hide show
  1. package/README.md +13 -9
  2. package/dist/application/errors.d.ts +1 -1
  3. package/dist/application/role-command-service.d.ts +29 -1
  4. package/dist/application/role-command-service.js +41 -2
  5. package/dist/application/role-creation-service.d.ts +32 -1
  6. package/dist/application/role-creation-service.js +56 -10
  7. package/dist/application/role-removal-service.d.ts +19 -0
  8. package/dist/application/role-removal-service.js +13 -3
  9. package/dist/application/session-mutations.d.ts +7 -0
  10. package/dist/application/session-mutations.js +8 -0
  11. package/dist/application/task-room-service.d.ts +262 -0
  12. package/dist/application/task-room-service.js +571 -0
  13. package/dist/atomic-file.d.ts +5 -3
  14. package/dist/atomic-file.js +32 -8
  15. package/dist/build-info.json +4 -4
  16. package/dist/cli.js +39 -15
  17. package/dist/config.d.ts +6 -3
  18. package/dist/config.js +0 -17
  19. package/dist/docs.d.ts +1 -1
  20. package/dist/docs.js +26 -4
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/owner-channel/attachments.d.ts +2 -4
  24. package/dist/owner-channel/attachments.js +6 -39
  25. package/dist/owner-channel/channel.d.ts +5 -0
  26. package/dist/owner-channel/channel.js +159 -21
  27. package/dist/owner-channel/commands.d.ts +43 -1
  28. package/dist/owner-channel/commands.js +137 -130
  29. package/dist/rooms-tasks/cli.js +301 -463
  30. package/dist/rooms-tasks/task-lists.d.ts +19 -0
  31. package/dist/rooms-tasks/task-lists.js +96 -0
  32. package/dist/rooms-tasks/task-state.d.ts +3 -0
  33. package/dist/rooms-tasks/task-state.js +281 -175
  34. package/dist/rooms-tasks/types.d.ts +11 -1
  35. package/dist/runner.js +10 -33
  36. package/dist/session/control.d.ts +10 -1
  37. package/dist/session/control.js +22 -21
  38. package/dist/watchdog/query.d.ts +2 -0
  39. package/dist/watchdog/query.js +7 -3
  40. package/dist/web/runtime.js +2 -0
  41. package/dist/web/server.d.ts +2 -0
  42. package/dist/web/server.js +88 -3
  43. package/package.json +1 -1
@@ -4,12 +4,15 @@ 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';
8
+ import { TaskListError } from '../rooms-tasks/task-lists.js';
7
9
  /** A malformed invocation; the dispatcher answers it with annotated help. */
8
10
  class OwnerCommandUsageError extends Error {
9
11
  }
10
12
  const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
11
13
  const REPLY_MAX_CHARS = 3_500;
12
14
  const taskListRecord = (task) => `${taskStatus(task.state)} ${markdownCode(task.task_id)} — ${markdownProse(task.title)}`
15
+ + ` — List ${markdownCode(task.list_name ?? 'default')}`
13
16
  + (task.blocked ? ` — 🚧 Blocked: ${markdownProse(task.blocked.reason)}` : '');
14
17
  const roomListRecord = (room) => `${roomStatus(room.state)} ${markdownCode(room.room_id)} — ${markdownProse(room.room_name)}`
15
18
  + (room.task_id ? ` — Task ${markdownCode(room.task_id)}` : '');
@@ -34,6 +37,14 @@ function isKnownOwnerRoomState(message) {
34
37
  ].some(pattern => pattern.test(message));
35
38
  }
36
39
  function ownerTaskFailure(error) {
40
+ if (error instanceof TaskListError)
41
+ return {
42
+ kind: error.code === 'list_not_found' ? 'not_found'
43
+ : error.code === 'invalid_name' ? 'usage' : 'state',
44
+ detail: error.message,
45
+ action: error.code === 'list_not_found' ? 'Run /task lists to find a valid list.'
46
+ : 'Choose a valid list name or an explicit different destination.',
47
+ };
37
48
  if (error instanceof TaskStateError) {
38
49
  const missing = /^task not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
39
50
  if (missing)
@@ -53,6 +64,12 @@ function ownerTaskFailure(error) {
53
64
  };
54
65
  }
55
66
  function ownerRoomFailure(error) {
67
+ if (error instanceof TaskRoomApplicationError
68
+ && (error.code === 'room_not_found' || error.code === 'room_record_not_found'))
69
+ return {
70
+ kind: 'not_found', detail: `Room ${error.fields.room} was not found.`,
71
+ action: 'Run /room list to find a valid room ID.',
72
+ };
56
73
  if (error instanceof RoomStateError) {
57
74
  const missing = /^room not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
58
75
  if (missing)
@@ -228,9 +245,8 @@ export const ownerCommands = [
228
245
  name: 'tasks', usage: '/tasks [state]',
229
246
  summary: 'list tasks (optionally filter by state)',
230
247
  execute: async (ctx, args) => {
231
- const { listTasks } = await import('../rooms-tasks/task-state.js');
232
248
  const stateFilter = args?.trim() || undefined;
233
- const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
249
+ const tasks = ctx.listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
234
250
  await ctx.reply(renderMarkdownList({
235
251
  icon: '📋', title: 'Tasks', empty: 'No tasks found.',
236
252
  records: tasks.map(taskListRecord),
@@ -250,14 +266,14 @@ export const ownerCommands = [
250
266
  const rest = firstLineTokens.slice(1);
251
267
  const trailingLines = argLines.slice(1).join('\n').trim() || undefined;
252
268
  const showTask = async (id) => {
253
- const { getTask } = await import('../rooms-tasks/task-state.js');
254
- const t = getTask(id);
269
+ const { task: t } = ctx.getTask(id);
255
270
  await ctx.reply(renderMarkdownResult({
256
271
  icon: '📋', title: 'Task details',
257
272
  fields: [
258
273
  { label: 'ID', value: t.task_id, kind: 'code' },
259
274
  { label: 'Title', value: t.title },
260
275
  { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
276
+ { label: 'List', value: t.list_name ?? 'default', kind: 'code' },
261
277
  ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
262
278
  ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
263
279
  ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
@@ -275,17 +291,15 @@ export const ownerCommands = [
275
291
  const tplFlag = rest.find(r => r.startsWith('--template='));
276
292
  const template = tplFlag ? tplFlag.slice('--template='.length) : undefined;
277
293
  const noRoom = rest.includes('--no-room');
278
- const titleParts = rest.filter(r => r !== '--backlog' && !r.startsWith('--template=') && r !== '--no-room');
294
+ const listFlag = rest.find(r => r.startsWith('--list='));
295
+ const list = listFlag?.slice('--list='.length);
296
+ const titleParts = rest.filter(r => r !== '--backlog' && !r.startsWith('--template=')
297
+ && !r.startsWith('--list=') && r !== '--no-room');
279
298
  if (!titleParts.length)
280
299
  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 } : {}),
300
+ const t = await ctx.createTask({
301
+ title: titleParts.join(' '), origin: { type: 'owner_channel' },
302
+ backlog, template, noRoom, brief: trailingLines, list,
289
303
  });
290
304
  await ctx.reply(taskAction('Task created', t, [
291
305
  { label: 'Title', value: t.title },
@@ -295,8 +309,10 @@ export const ownerCommands = [
295
309
  break;
296
310
  }
297
311
  case 'list': {
298
- const { listTasks } = await import('../rooms-tasks/task-state.js');
299
- const filter = rest[0];
312
+ const listFlag = rest.find(r => r.startsWith('--list='));
313
+ const list = trailingLines ?? listFlag?.slice('--list='.length);
314
+ const grouped = rest.includes('--group-by-list');
315
+ const filter = rest.find(r => !r.startsWith('--'));
300
316
  const stateMap = {
301
317
  backlog: 'backlog', active: 'active', blocked: 'active',
302
318
  done: 'done', all: ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'],
@@ -304,7 +320,8 @@ export const ownerCommands = [
304
320
  const stateFilter = filter && stateMap[filter]
305
321
  ? { state: stateMap[filter] }
306
322
  : undefined;
307
- const tasks = listTasks(stateFilter);
323
+ const combined = { ...(stateFilter ?? {}), ...(list ? { list } : {}) };
324
+ const tasks = ctx.listTasks(combined);
308
325
  if (filter === 'blocked') {
309
326
  const blocked = tasks.filter(t => t.blocked);
310
327
  await ctx.reply(renderMarkdownList({
@@ -313,12 +330,63 @@ export const ownerCommands = [
313
330
  }));
314
331
  break;
315
332
  }
333
+ if (grouped) {
334
+ const groups = ctx.groupedTasks(combined);
335
+ await ctx.reply(groups.map(group => renderMarkdownList({
336
+ icon: '📋', title: `Tasks — ${group.list.name}`, empty: 'No tasks found.',
337
+ records: group.tasks.map(taskListRecord),
338
+ })).join('\n\n'));
339
+ break;
340
+ }
316
341
  await ctx.reply(renderMarkdownList({
317
342
  icon: '📋', title: 'Tasks', empty: 'No tasks found.',
318
343
  records: tasks.map(taskListRecord),
319
344
  }));
320
345
  break;
321
346
  }
347
+ case 'lists': {
348
+ const lists = ctx.listTaskLists();
349
+ await ctx.reply(renderMarkdownList({ icon: '📚', title: 'Task lists', empty: 'No task lists found.',
350
+ records: lists.map(list => `${markdownCode(list.name)}${list.built_in ? ' — built-in' : ''}`) }));
351
+ break;
352
+ }
353
+ case 'list-create': {
354
+ const name = trailingLines ?? rest.join(' ');
355
+ if (!name)
356
+ throw new OwnerCommandUsageError('usage: /task list-create\n<name>');
357
+ const list = await ctx.createTaskList(name);
358
+ await ctx.reply(renderMarkdownResult({ icon: '📚', title: 'Task list created', fields: [{ label: 'Name', value: list.name }] }));
359
+ break;
360
+ }
361
+ case 'list-rename': {
362
+ const names = argLines.slice(1).map(line => line.trim()).filter(Boolean);
363
+ const current = names.length === 2 ? names[0] : rest[0];
364
+ const next = names.length === 2 ? names[1] : rest[1];
365
+ if (!current || !next)
366
+ throw new OwnerCommandUsageError('usage: /task list-rename\n<name>\n<new-name>');
367
+ const list = await ctx.renameTaskList(current, next);
368
+ await ctx.reply(renderMarkdownResult({ icon: '📚', title: 'Task list renamed', fields: [{ label: 'Name', value: list.name }] }));
369
+ break;
370
+ }
371
+ case 'list-delete': {
372
+ const names = argLines.slice(1).map(line => line.trim()).filter(Boolean);
373
+ const name = names[0] ?? rest[0];
374
+ const destination = names[1] ?? rest.find(r => r.startsWith('--move-to='))?.slice('--move-to='.length);
375
+ if (!name)
376
+ throw new OwnerCommandUsageError('usage: /task list-delete\n<name>\n[move-to-name]');
377
+ const result = await ctx.deleteTaskList(name, destination);
378
+ await ctx.reply(renderMarkdownResult({ icon: '🗑️', title: 'Task list deleted', fields: [
379
+ { label: 'Name', value: result.deleted.name }, { label: 'Tasks moved', value: String(result.moved) },
380
+ ] }));
381
+ break;
382
+ }
383
+ case 'move': {
384
+ const destination = trailingLines ?? rest.find(r => r.startsWith('--list='))?.slice('--list='.length);
385
+ if (!rest[0] || !destination)
386
+ throw new OwnerCommandUsageError('usage: /task move <id> --list=<name>');
387
+ await showTask((await ctx.moveTask(rest[0], destination)).task_id);
388
+ break;
389
+ }
322
390
  case 'show': {
323
391
  if (!rest[0])
324
392
  throw new OwnerCommandUsageError('usage: /task show <id>');
@@ -328,33 +396,29 @@ export const ownerCommands = [
328
396
  case 'start': {
329
397
  if (!rest[0])
330
398
  throw new OwnerCommandUsageError('usage: /task start <id>');
331
- const { startTask } = await import('../rooms-tasks/task-state.js');
332
- const t = startTask(rest[0]);
399
+ const t = await ctx.startTask(rest[0]);
333
400
  await ctx.reply(taskAction('Task started', t));
334
401
  break;
335
402
  }
336
403
  case 'block': {
337
404
  if (!rest[0] || !rest[1])
338
405
  throw new OwnerCommandUsageError('usage: /task block <id> <reason>');
339
- const { blockTask } = await import('../rooms-tasks/task-state.js');
340
406
  const reason = rest.slice(1).join(' ');
341
- const t = blockTask(rest[0], reason);
407
+ const t = ctx.blockTask(rest[0], reason);
342
408
  await ctx.reply(taskAction('Task blocked', t, [{ label: 'Reason', value: reason }]));
343
409
  break;
344
410
  }
345
411
  case 'unblock': {
346
412
  if (!rest[0])
347
413
  throw new OwnerCommandUsageError('usage: /task unblock <id>');
348
- const { unblockTask } = await import('../rooms-tasks/task-state.js');
349
- const t = unblockTask(rest[0]);
414
+ const t = ctx.unblockTask(rest[0]);
350
415
  await ctx.reply(taskAction('Task unblocked', t));
351
416
  break;
352
417
  }
353
418
  case 'review': {
354
419
  if (!rest[0])
355
420
  throw new OwnerCommandUsageError('usage: /task review <id>');
356
- const { reviewTask } = await import('../rooms-tasks/task-state.js');
357
- const t = reviewTask(rest[0]);
421
+ const t = ctx.reviewTask(rest[0]);
358
422
  await ctx.reply(taskAction('Task ready for review', t));
359
423
  break;
360
424
  }
@@ -374,8 +438,7 @@ export const ownerCommands = [
374
438
  case 'delete': {
375
439
  if (rest.length !== 2 || rest[0] !== rest[1])
376
440
  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]);
441
+ const deleted = ctx.deleteTask(rest[0]);
379
442
  await ctx.reply(renderMarkdownResult({
380
443
  icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
381
444
  fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
@@ -385,40 +448,7 @@ export const ownerCommands = [
385
448
  case 'recover': {
386
449
  if (!rest[0])
387
450
  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
- }));
451
+ await ctx.recoverTask(rest[0]);
422
452
  break;
423
453
  }
424
454
  default:
@@ -442,11 +472,11 @@ export const ownerCommands = [
442
472
  {
443
473
  name: 'rooms', summary: 'list rooms',
444
474
  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');
475
+ const rooms = await ctx.listRoomQueries();
447
476
  await ctx.reply(renderMarkdownList({
448
477
  icon: '🏠', title: 'Rooms', empty: 'No rooms found.',
449
- records: rooms.map(roomListRecord),
478
+ records: rooms.map(room => `${roomStatus(room.state)} ${markdownCode(room.room_id)} — ${markdownProse(room.room_name)}`
479
+ + (room.orchestration?.task_id ? ` — Task ${markdownCode(room.orchestration.task_id)}` : '')),
450
480
  }));
451
481
  }),
452
482
  },
@@ -463,24 +493,28 @@ export const ownerCommands = [
463
493
  const rest = firstLineTokens.slice(1);
464
494
  const roomTrailingLines = argLines.slice(1).join('\n').trim() || undefined;
465
495
  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
- }));
496
+ let detail;
497
+ try {
498
+ detail = await ctx.getRoomQuery(id);
499
+ }
500
+ catch (error) {
501
+ if (!(error instanceof TaskRoomApplicationError && error.code === 'room_not_found'))
502
+ throw error;
503
+ return ctx.reply(renderMarkdownFailure({ kind: 'not_found', subject: `/room show ${id}`,
504
+ detail: `Room ${id} was not found.`, action: 'Run /room list to find a valid room ID.' }));
505
+ }
506
+ const { room, orchestration: r } = detail;
473
507
  await ctx.reply(renderMarkdownResult({
474
508
  icon: '🏠', title: 'Room details',
475
509
  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' },
510
+ { label: 'ID', value: room.room_id, kind: 'code' },
511
+ { label: 'Name', value: room.room_name },
512
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
513
+ ...(r ? [{ label: 'Saga', value: `${r.saga.phase} (step ${r.saga.step_index})` }] : []),
514
+ ...(r?.task_id ? [{ label: 'Task', value: r.task_id, kind: 'code' }] : []),
515
+ ...(r?.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
516
+ ...(r?.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
517
+ ...(r ? [{ label: 'Created', value: r.created_at, kind: 'code' }] : []),
484
518
  ],
485
519
  }));
486
520
  };
@@ -492,22 +526,16 @@ export const ownerCommands = [
492
526
  const nameParts = rest.filter(r => !r.startsWith('--'));
493
527
  if (!nameParts.length)
494
528
  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)
529
+ let r;
530
+ try {
531
+ r = await ctx.createRoom({ name: nameParts.join(' '), template: templateName,
532
+ goal: roomTrailingLines });
533
+ }
534
+ catch (error) {
535
+ if (error instanceof TaskRoomApplicationError && error.code === 'template_not_found')
502
536
  throw new OwnerCommandUsageError(`unknown template: ${templateName}`);
503
- templateSnapshot = snapshotTemplate(tpl);
537
+ throw error;
504
538
  }
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
539
  await ctx.reply(roomAction('Room created', r, [
512
540
  { label: 'Name', value: r.room_name },
513
541
  ...(templateName ? [{ label: 'Template', value: templateName, kind: 'code' }] : []),
@@ -515,14 +543,14 @@ export const ownerCommands = [
515
543
  break;
516
544
  }
517
545
  case 'list': {
518
- const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
519
546
  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');
547
+ if (filter && !['active', 'provisioning', 'all'].includes(filter))
548
+ throw new OwnerCommandUsageError('usage: /room list [active|provisioning|all]');
549
+ const rooms = await ctx.listRoomQueries(filter === 'active' || filter === 'provisioning' ? { state: filter } : undefined);
523
550
  await ctx.reply(renderMarkdownList({
524
551
  icon: '🏠', title: 'Rooms', empty: 'No rooms found.',
525
- records: rooms.map(roomListRecord),
552
+ records: rooms.map(room => `${roomStatus(room.state)} ${markdownCode(room.room_id)} — ${markdownProse(room.room_name)}`
553
+ + (room.orchestration?.task_id ? ` — Task ${markdownCode(room.orchestration.task_id)}` : '')),
526
554
  }));
527
555
  break;
528
556
  }
@@ -542,32 +570,7 @@ export const ownerCommands = [
542
570
  case 'recover': {
543
571
  if (!rest[0])
544
572
  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
- }));
573
+ await ctx.recoverRoom(rest[0]);
571
574
  break;
572
575
  }
573
576
  default:
@@ -592,8 +595,7 @@ export const ownerCommands = [
592
595
  name: 'templates', aliases: ['template-list'],
593
596
  summary: 'list available room templates',
594
597
  execute: noArgs('/templates', async (ctx) => {
595
- const { listTemplates } = await import('../rooms-tasks/templates.js');
596
- const templates = listTemplates({});
598
+ const templates = ctx.listTemplateQueries();
597
599
  if (!templates.length)
598
600
  return ctx.reply('📐 No templates.');
599
601
  const lines = templates.map(t => {
@@ -613,10 +615,15 @@ export const ownerCommands = [
613
615
  const parts = args.trim().split(/\s+/);
614
616
  const sub = parts[0];
615
617
  const showTemplate = async (nameStr) => {
616
- const { resolveTemplate } = await import('../rooms-tasks/templates.js');
617
- const t = resolveTemplate(nameStr, {});
618
- if (!t)
618
+ let t;
619
+ try {
620
+ t = ctx.getTemplateQuery(nameStr);
621
+ }
622
+ catch (error) {
623
+ if (!(error instanceof TaskRoomApplicationError && error.code === 'template_not_found'))
624
+ throw error;
619
625
  return ctx.reply(`⚠️ template not found: ${nameStr}`);
626
+ }
620
627
  const lines = [
621
628
  `📐 Template: ${t.name}@${t.version}`,
622
629
  `Description: ${t.description}`,
@@ -635,8 +642,7 @@ export const ownerCommands = [
635
642
  break;
636
643
  }
637
644
  case 'list': {
638
- const { listTemplates } = await import('../rooms-tasks/templates.js');
639
- const templates = listTemplates({});
645
+ const templates = ctx.listTemplateQueries();
640
646
  if (!templates.length)
641
647
  return ctx.reply('📐 No templates.');
642
648
  const lines = templates.map(t => {
@@ -710,5 +716,6 @@ export function fleetCliOps(role, configPath) {
710
716
  }),
711
717
  closeRoom: roomId => launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath),
712
718
  settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
719
+ recoverTask: taskId => launchFleetWorker(['task', '_recover', taskId], `task-recover-${taskId}`, configPath),
713
720
  };
714
721
  }