@ours.network/fleet 0.19.0-nightly.9 โ†’ 1.0.0

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 (114) hide show
  1. package/README.md +66 -11
  2. package/dist/application/fleet-query-service.d.ts +1 -1
  3. package/dist/application/fleet-query-service.js +12 -0
  4. package/dist/application/role-creation-service.js +3 -1
  5. package/dist/application/types.d.ts +11 -0
  6. package/dist/briefing.d.ts +1 -1
  7. package/dist/briefing.js +103 -10
  8. package/dist/build-info.json +5 -5
  9. package/dist/cli.js +41 -11
  10. package/dist/config.d.ts +21 -7
  11. package/dist/config.js +32 -18
  12. package/dist/creation.d.ts +1 -1
  13. package/dist/docs.d.ts +2 -2
  14. package/dist/docs.js +117 -24
  15. package/dist/doctor.js +10 -10
  16. package/dist/duration.js +1 -1
  17. package/dist/fleet-proxy.d.ts +5 -0
  18. package/dist/harness/acp-agent.js +11 -6
  19. package/dist/harness/claude-code.js +201 -8
  20. package/dist/harness/codex.d.ts +4 -1
  21. package/dist/harness/codex.js +72 -13
  22. package/dist/harness/types.d.ts +57 -6
  23. package/dist/isolation/bubblewrap.d.ts +4 -4
  24. package/dist/isolation/bubblewrap.js +4 -4
  25. package/dist/isolation/policy.d.ts +1 -1
  26. package/dist/isolation/policy.js +2 -2
  27. package/dist/isolation/registry.d.ts +3 -3
  28. package/dist/isolation/registry.js +5 -5
  29. package/dist/isolation/resources.d.ts +1 -1
  30. package/dist/isolation/resources.js +1 -1
  31. package/dist/isolation/types.d.ts +3 -3
  32. package/dist/loops/manager.d.ts +30 -1
  33. package/dist/loops/manager.js +69 -6
  34. package/dist/loops/state.d.ts +18 -0
  35. package/dist/loops/state.js +4 -0
  36. package/dist/model-env.d.ts +71 -0
  37. package/dist/model-env.js +106 -0
  38. package/dist/monitor.d.ts +2 -2
  39. package/dist/monitor.js +4 -4
  40. package/dist/ops.d.ts +2 -2
  41. package/dist/ops.js +9 -9
  42. package/dist/owner-channel/channel.d.ts +17 -0
  43. package/dist/owner-channel/channel.js +104 -23
  44. package/dist/owner-channel/commands.d.ts +9 -0
  45. package/dist/owner-channel/commands.js +226 -99
  46. package/dist/owner-channel/notices.d.ts +7 -0
  47. package/dist/owner-channel/notices.js +9 -0
  48. package/dist/rooms-tasks/cli.js +731 -219
  49. package/dist/rooms-tasks/close.d.ts +35 -0
  50. package/dist/rooms-tasks/close.js +182 -0
  51. package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
  52. package/dist/rooms-tasks/cowork-adapter.js +116 -0
  53. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  54. package/dist/rooms-tasks/external-worker.js +40 -0
  55. package/dist/rooms-tasks/index.d.ts +2 -0
  56. package/dist/rooms-tasks/index.js +2 -0
  57. package/dist/rooms-tasks/markdown.d.ts +49 -0
  58. package/dist/rooms-tasks/markdown.js +206 -0
  59. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  60. package/dist/rooms-tasks/member-startup.js +151 -0
  61. package/dist/rooms-tasks/provision.d.ts +8 -0
  62. package/dist/rooms-tasks/provision.js +363 -97
  63. package/dist/rooms-tasks/room-state.d.ts +13 -1
  64. package/dist/rooms-tasks/room-state.js +149 -1
  65. package/dist/rooms-tasks/task-state.d.ts +13 -2
  66. package/dist/rooms-tasks/task-state.js +131 -3
  67. package/dist/rooms-tasks/terminal.d.ts +23 -0
  68. package/dist/rooms-tasks/terminal.js +56 -0
  69. package/dist/rooms-tasks/types.d.ts +108 -2
  70. package/dist/runner.d.ts +48 -0
  71. package/dist/runner.js +252 -100
  72. package/dist/session/acp.d.ts +106 -2
  73. package/dist/session/acp.js +217 -14
  74. package/dist/session/activity.d.ts +31 -0
  75. package/dist/session/activity.js +48 -0
  76. package/dist/session/control.d.ts +1 -1
  77. package/dist/session/conversation-normalizer.d.ts +7 -1
  78. package/dist/session/conversation-normalizer.js +154 -11
  79. package/dist/session/conversation-store.js +1 -1
  80. package/dist/session/conversation-types.d.ts +25 -6
  81. package/dist/session/types.d.ts +35 -0
  82. package/dist/spawn.d.ts +4 -2
  83. package/dist/spawn.js +39 -26
  84. package/dist/supervisor/launchd.d.ts +2 -2
  85. package/dist/supervisor/launchd.js +4 -4
  86. package/dist/supervisor/systemd.js +4 -4
  87. package/dist/supervisor/types.d.ts +1 -1
  88. package/dist/temp-lifecycle.d.ts +14 -2
  89. package/dist/temp-lifecycle.js +90 -0
  90. package/dist/watchdog/alerts.d.ts +2 -2
  91. package/dist/watchdog/alerts.js +2 -2
  92. package/dist/watchdog/briefing.d.ts +5 -5
  93. package/dist/watchdog/briefing.js +13 -6
  94. package/dist/watchdog/config.js +1 -1
  95. package/dist/watchdog/query.d.ts +2 -2
  96. package/dist/watchdog/query.js +2 -2
  97. package/dist/watchdog/report.js +2 -2
  98. package/dist/watchdog/run.d.ts +2 -2
  99. package/dist/watchdog/run.js +6 -6
  100. package/dist/watchdog/scheduler.d.ts +7 -7
  101. package/dist/watchdog/scheduler.js +10 -10
  102. package/dist/watchdog/service.d.ts +4 -4
  103. package/dist/watchdog/service.js +5 -6
  104. package/dist/watchdog/store.d.ts +4 -4
  105. package/dist/watchdog/store.js +4 -4
  106. package/dist/web/runtime.js +1 -1
  107. package/dist/web/server.js +1 -1
  108. package/dist/web/topology-promote.js +1 -1
  109. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js โ†’ TerminalView-C_G1ID2P.js} +1 -1
  110. package/dist/web-app/assets/{index-C3S-xFRU.js โ†’ index-BCBK78hw.js} +5 -5
  111. package/dist/web-app/index.html +1 -1
  112. package/dist/worklog.d.ts +7 -1
  113. package/dist/worklog.js +191 -39
  114. package/package.json +1 -1
@@ -1,10 +1,92 @@
1
1
  import { execFile, spawn } from 'node:child_process';
2
+ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
3
+ import { RoomStateError } from '../rooms-tasks/room-state.js';
4
+ import { TaskStateError } from '../rooms-tasks/task-state.js';
5
+ import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
2
6
  import { OWNER_COMMENT_LABEL, ownerNotices } from './notices.js';
3
7
  /** A malformed invocation; the dispatcher answers it with annotated help. */
4
8
  class OwnerCommandUsageError extends Error {
5
9
  }
6
10
  const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
11
  const REPLY_MAX_CHARS = 3_500;
12
+ const taskListRecord = (task) => `${taskStatus(task.state)} ${markdownCode(task.task_id)} โ€” ${markdownProse(task.title)}`
13
+ + (task.blocked ? ` โ€” ๐Ÿšง Blocked: ${markdownProse(task.blocked.reason)}` : '');
14
+ const roomListRecord = (room) => `${roomStatus(room.state)} ${markdownCode(room.room_id)} โ€” ${markdownProse(room.room_name)}`
15
+ + (room.task_id ? ` โ€” Task ${markdownCode(room.task_id)}` : '');
16
+ const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
17
+ const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
18
+ function isKnownOwnerTaskState(message) {
19
+ return [
20
+ new RegExp(`^cannot transition from '${TASK_STATE_WORD}' to '${TASK_STATE_WORD}'$`, 'u'),
21
+ new RegExp(`^task ${SAFE_ID_WORD} has a pending '(?:done|cancelled)' terminal intent$`, 'u'),
22
+ new RegExp(`^cannot (?:block|cancel) a '${TASK_STATE_WORD}' task$`, 'u'),
23
+ /^task is not blocked$/u,
24
+ new RegExp(`^task ${SAFE_ID_WORD} already has a conflicting '(?:done|cancelled)' terminal intent$`, 'u'),
25
+ new RegExp(`^task ${SAFE_ID_WORD} is already in terminal state '${TASK_STATE_WORD}'$`, 'u'),
26
+ new RegExp(`^cannot delete a '${TASK_STATE_WORD}' task; only 'done' tasks can be deleted$`, 'u'),
27
+ ].some(pattern => pattern.test(message));
28
+ }
29
+ function isKnownOwnerRoomState(message) {
30
+ return [
31
+ new RegExp(`^room ${SAFE_ID_WORD} is not closing$`, 'u'),
32
+ new RegExp(`^room ${SAFE_ID_WORD} has no recorded member .{1,128}$`, 'u'),
33
+ new RegExp(`^room ${SAFE_ID_WORD} close cannot move backward to [A-Za-z_]+$`, 'u'),
34
+ ].some(pattern => pattern.test(message));
35
+ }
36
+ function ownerTaskFailure(error) {
37
+ if (error instanceof TaskStateError) {
38
+ const missing = /^task not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
39
+ if (missing)
40
+ return {
41
+ kind: 'not_found', detail: `Task ${missing[1]} was not found.`,
42
+ action: 'Run /task list to find a valid task ID.',
43
+ };
44
+ if (isKnownOwnerTaskState(error.message))
45
+ return {
46
+ kind: 'state', detail: 'The current task state does not allow that action.',
47
+ action: 'Run /task show <id> to inspect the current task state.',
48
+ };
49
+ }
50
+ return {
51
+ kind: 'unexpected',
52
+ action: 'Retry once; if it repeats, inspect the role logs.',
53
+ };
54
+ }
55
+ function ownerRoomFailure(error) {
56
+ if (error instanceof RoomStateError) {
57
+ const missing = /^room not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
58
+ if (missing)
59
+ return {
60
+ kind: 'not_found', detail: `Room ${missing[1]} was not found.`,
61
+ action: 'Run /room list to find a valid room ID.',
62
+ };
63
+ if (isKnownOwnerRoomState(error.message))
64
+ return {
65
+ kind: 'state', detail: 'The current room state does not allow that action.',
66
+ action: 'Run /room show <id> to inspect the current room state.',
67
+ };
68
+ }
69
+ return {
70
+ kind: 'unexpected',
71
+ action: 'Retry once; if it repeats, inspect the role logs.',
72
+ };
73
+ }
74
+ const taskAction = (title, task, fields = []) => renderMarkdownResult({
75
+ icon: '๐Ÿ“‹', title,
76
+ fields: [
77
+ { label: 'ID', value: task.task_id, kind: 'code' },
78
+ { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
79
+ ...fields,
80
+ ],
81
+ });
82
+ const roomAction = (title, room, fields = []) => renderMarkdownResult({
83
+ icon: '๐Ÿ ', title,
84
+ fields: [
85
+ { label: 'ID', value: room.room_id, kind: 'code' },
86
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
87
+ ...fields,
88
+ ],
89
+ });
8
90
  const strip = (value, max) => String(value).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, max);
9
91
  /** Like `strip`, but keeps newlines so multi-line listings stay readable. */
10
92
  const stripMultiline = (value, max) => String(value).replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, ' ').slice(0, max);
@@ -149,18 +231,15 @@ export const ownerCommands = [
149
231
  const { listTasks } = await import('../rooms-tasks/task-state.js');
150
232
  const stateFilter = args?.trim() || undefined;
151
233
  const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
152
- if (!tasks.length)
153
- return ctx.reply('๐Ÿ“‹ No tasks.');
154
- const lines = tasks.map(t => {
155
- const blocked = t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : '';
156
- return `${t.task_id} ${t.state}${blocked} ${t.title}`;
157
- });
158
- await ctx.reply(tail(`๐Ÿ“‹ Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
234
+ await ctx.reply(renderMarkdownList({
235
+ icon: '๐Ÿ“‹', title: 'Tasks', empty: 'No tasks found.',
236
+ records: tasks.map(taskListRecord),
237
+ }));
159
238
  },
160
239
  },
161
240
  {
162
241
  name: 'task',
163
- usage: '/task <create|list|show|start|block|unblock|review|done|cancel|recover> ...',
242
+ usage: '/task <create|list|show|start|block|unblock|review|done|cancel|delete|recover> ...',
164
243
  summary: 'task lifecycle subcommands',
165
244
  execute: async (ctx, args) => {
166
245
  if (!args)
@@ -173,16 +252,19 @@ export const ownerCommands = [
173
252
  const showTask = async (id) => {
174
253
  const { getTask } = await import('../rooms-tasks/task-state.js');
175
254
  const t = getTask(id);
176
- const lines = [
177
- `๐Ÿ“‹ Task: ${t.task_id}`,
178
- `Title: ${t.title}`,
179
- `State: ${t.state}${t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : ''}`,
180
- ...(t.template ? [`Template: ${t.template.name}@${t.template.version}`] : []),
181
- ...(t.room_id ? [`Room: ${t.room_id}`] : []),
182
- `Origin: ${t.origin.type}`,
183
- `Created: ${t.created_at}`,
184
- ];
185
- await ctx.reply(lines.join('\n'));
255
+ await ctx.reply(renderMarkdownResult({
256
+ icon: '๐Ÿ“‹', title: 'Task details',
257
+ fields: [
258
+ { label: 'ID', value: t.task_id, kind: 'code' },
259
+ { label: 'Title', value: t.title },
260
+ { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
261
+ ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
262
+ ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
263
+ ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
264
+ { label: 'Origin', value: t.origin.type },
265
+ { label: 'Created', value: t.created_at, kind: 'code' },
266
+ ],
267
+ }));
186
268
  };
187
269
  try {
188
270
  switch (sub) {
@@ -205,12 +287,11 @@ export const ownerCommands = [
205
287
  ...(noRoom ? { no_room: true } : {}),
206
288
  ...(trailingLines ? { brief: trailingLines } : {}),
207
289
  });
208
- const lines = [`Task ${t.task_id} created ยท ${t.state}`];
209
- if (template)
210
- lines.push(`Template: ${template}`);
211
- if (noRoom)
212
- lines.push('No room requested');
213
- await ctx.reply(lines.join('\n'));
290
+ await ctx.reply(taskAction('Task created', t, [
291
+ { label: 'Title', value: t.title },
292
+ ...(template ? [{ label: 'Template', value: template, kind: 'code' }] : []),
293
+ ...(noRoom ? [{ label: 'Room', value: 'Not requested' }] : []),
294
+ ]));
214
295
  break;
215
296
  }
216
297
  case 'list': {
@@ -226,20 +307,16 @@ export const ownerCommands = [
226
307
  const tasks = listTasks(stateFilter);
227
308
  if (filter === 'blocked') {
228
309
  const blocked = tasks.filter(t => t.blocked);
229
- if (!blocked.length) {
230
- await ctx.reply('๐Ÿ“‹ No blocked tasks.');
231
- break;
232
- }
233
- const lines = blocked.map(t => `${t.task_id} ${t.state} ${t.title} [BLOCKED: ${t.blocked.reason}]`);
234
- await ctx.reply(tail(`๐Ÿ“‹ Blocked tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
235
- break;
236
- }
237
- if (!tasks.length) {
238
- await ctx.reply('๐Ÿ“‹ No tasks.');
310
+ await ctx.reply(renderMarkdownList({
311
+ icon: '๐Ÿšง', title: 'Blocked tasks', empty: 'No blocked tasks found.',
312
+ records: blocked.map(taskListRecord),
313
+ }));
239
314
  break;
240
315
  }
241
- const lines = tasks.map(t => `${t.task_id} ${t.state} ${t.title}${t.blocked ? ` [BLOCKED]` : ''}`);
242
- await ctx.reply(tail(`๐Ÿ“‹ Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
316
+ await ctx.reply(renderMarkdownList({
317
+ icon: '๐Ÿ“‹', title: 'Tasks', empty: 'No tasks found.',
318
+ records: tasks.map(taskListRecord),
319
+ }));
243
320
  break;
244
321
  }
245
322
  case 'show': {
@@ -253,7 +330,7 @@ export const ownerCommands = [
253
330
  throw new OwnerCommandUsageError('usage: /task start <id>');
254
331
  const { startTask } = await import('../rooms-tasks/task-state.js');
255
332
  const t = startTask(rest[0]);
256
- await ctx.reply(`โœ… Task ${t.task_id} โ†’ provisioning`);
333
+ await ctx.reply(taskAction('Task started', t));
257
334
  break;
258
335
  }
259
336
  case 'block': {
@@ -262,7 +339,7 @@ export const ownerCommands = [
262
339
  const { blockTask } = await import('../rooms-tasks/task-state.js');
263
340
  const reason = rest.slice(1).join(' ');
264
341
  const t = blockTask(rest[0], reason);
265
- await ctx.reply(`๐Ÿšง Task ${t.task_id} blocked: ${reason}`);
342
+ await ctx.reply(taskAction('Task blocked', t, [{ label: 'Reason', value: reason }]));
266
343
  break;
267
344
  }
268
345
  case 'unblock': {
@@ -270,7 +347,7 @@ export const ownerCommands = [
270
347
  throw new OwnerCommandUsageError('usage: /task unblock <id>');
271
348
  const { unblockTask } = await import('../rooms-tasks/task-state.js');
272
349
  const t = unblockTask(rest[0]);
273
- await ctx.reply(`โœ… Task ${t.task_id} unblocked`);
350
+ await ctx.reply(taskAction('Task unblocked', t));
274
351
  break;
275
352
  }
276
353
  case 'review': {
@@ -278,24 +355,31 @@ export const ownerCommands = [
278
355
  throw new OwnerCommandUsageError('usage: /task review <id>');
279
356
  const { reviewTask } = await import('../rooms-tasks/task-state.js');
280
357
  const t = reviewTask(rest[0]);
281
- await ctx.reply(`๐Ÿ“ Task ${t.task_id} โ†’ review`);
358
+ await ctx.reply(taskAction('Task ready for review', t));
282
359
  break;
283
360
  }
284
361
  case 'done': {
285
362
  if (!rest[0])
286
363
  throw new OwnerCommandUsageError('usage: /task done <id> [summary]');
287
- const { completeTask } = await import('../rooms-tasks/task-state.js');
288
364
  const summary = rest.slice(1).join(' ') || undefined;
289
- const t = completeTask(rest[0], summary ? { summary } : undefined);
290
- await ctx.reply(`โœ… Task ${t.task_id} โ†’ done`);
365
+ await ctx.terminalTask(rest[0], 'done', summary ? { summary } : undefined);
291
366
  break;
292
367
  }
293
368
  case 'cancel': {
294
369
  if (rest.length < 2 || rest[0] !== rest[1])
295
370
  throw new OwnerCommandUsageError('destructive: /task cancel <id> <id> โ€” provide the task ID twice');
296
- const { cancelTask } = await import('../rooms-tasks/task-state.js');
297
- const t = cancelTask(rest[0]);
298
- await ctx.reply(`๐Ÿ—‘๏ธ Task ${t.task_id} โ†’ cancelled`);
371
+ await ctx.terminalTask(rest[0], 'cancelled');
372
+ break;
373
+ }
374
+ case 'delete': {
375
+ if (rest.length !== 2 || rest[0] !== rest[1])
376
+ 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]);
379
+ await ctx.reply(renderMarkdownResult({
380
+ icon: '๐Ÿ—‘๏ธ', title: deleted ? 'Task deleted' : 'Task already absent',
381
+ fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
382
+ }));
299
383
  break;
300
384
  }
301
385
  case 'recover': {
@@ -304,6 +388,10 @@ export const ownerCommands = [
304
388
  const { getTask } = await import('../rooms-tasks/task-state.js');
305
389
  const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
306
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
+ }
307
395
  const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
308
396
  const hints = [];
309
397
  if (t.state === 'provisioning' && room) {
@@ -316,14 +404,21 @@ export const ownerCommands = [
316
404
  if (room.provisioning_detail === 'member_failed')
317
405
  hints.push(`Member failed at step ${room.saga.step_index}`);
318
406
  }
319
- const lines = [`Task ${t.task_id} ยท ${t.state}`];
320
- if (room)
321
- lines.push(`Room ${room.room_id} ยท ${room.state} ยท saga: ${room.saga.phase}`);
322
- if (hints.length)
323
- lines.push(`Hints: ${hints.join('; ')}`);
324
- else
325
- lines.push('No automated recovery actions available.');
326
- await ctx.reply(lines.join('\n'));
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
+ }));
327
422
  break;
328
423
  }
329
424
  default:
@@ -334,7 +429,13 @@ export const ownerCommands = [
334
429
  catch (e) {
335
430
  if (e instanceof OwnerCommandUsageError)
336
431
  throw e;
337
- await ctx.reply(`โš ๏ธ ${e instanceof Error ? e.message : String(e)}`);
432
+ const failure = ownerTaskFailure(e);
433
+ await ctx.reply(renderMarkdownFailure({
434
+ kind: failure.kind,
435
+ subject: `/task ${sub}`,
436
+ ...(failure.detail ? { detail: failure.detail } : {}),
437
+ action: failure.action,
438
+ }));
338
439
  }
339
440
  },
340
441
  },
@@ -342,16 +443,16 @@ export const ownerCommands = [
342
443
  name: 'rooms', summary: 'list rooms',
343
444
  execute: noArgs('/rooms', async (ctx) => {
344
445
  const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
345
- const rooms = listRoomRecords();
346
- if (!rooms.length)
347
- return ctx.reply('๐Ÿ  No rooms.');
348
- const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
349
- await ctx.reply(tail(`๐Ÿ  Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
446
+ const rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
447
+ await ctx.reply(renderMarkdownList({
448
+ icon: '๐Ÿ ', title: 'Rooms', empty: 'No rooms found.',
449
+ records: rooms.map(roomListRecord),
450
+ }));
350
451
  }),
351
452
  },
352
453
  {
353
454
  name: 'room',
354
- usage: '/room <create|list|show|close|recover> ...',
455
+ usage: '/room <create|list|show|delete|close|recover> ...',
355
456
  summary: 'room lifecycle subcommands',
356
457
  execute: async (ctx, args) => {
357
458
  if (!args)
@@ -364,19 +465,24 @@ export const ownerCommands = [
364
465
  const showRoom = async (id) => {
365
466
  const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
366
467
  const r = getRoomRecord(id);
367
- if (!r)
368
- return ctx.reply(`โš ๏ธ room not found: ${id}`);
369
- const lines = [
370
- `๐Ÿ  Room: ${r.room_id}`,
371
- `Name: ${r.room_name}`,
372
- `State: ${r.state}`,
373
- `Saga: ${r.saga.phase} (step ${r.saga.step_index})`,
374
- ...(r.task_id ? [`Task: ${r.task_id}`] : []),
375
- ...(r.provisioning_detail ? [`Detail: ${r.provisioning_detail}`] : []),
376
- ...(r.saga.error ? [`Error: ${r.saga.error}`] : []),
377
- `Created: ${r.created_at}`,
378
- ];
379
- await ctx.reply(lines.join('\n'));
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
+ }));
473
+ await ctx.reply(renderMarkdownResult({
474
+ icon: '๐Ÿ ', title: 'Room details',
475
+ 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' },
484
+ ],
485
+ }));
380
486
  };
381
487
  try {
382
488
  switch (sub) {
@@ -402,24 +508,22 @@ export const ownerCommands = [
402
508
  ...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
403
509
  ...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
404
510
  });
405
- const lines = [`๐Ÿ  Room ${r.room_id} created ยท ${r.state}`];
406
- if (templateName)
407
- lines.push(`Template: ${templateName}`);
408
- await ctx.reply(lines.join('\n'));
511
+ await ctx.reply(roomAction('Room created', r, [
512
+ { label: 'Name', value: r.room_name },
513
+ ...(templateName ? [{ label: 'Template', value: templateName, kind: 'code' }] : []),
514
+ ]));
409
515
  break;
410
516
  }
411
517
  case 'list': {
412
518
  const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
413
519
  const filter = rest[0];
414
- let rooms = listRoomRecords();
520
+ let rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
415
521
  if (filter === 'active')
416
522
  rooms = rooms.filter(r => r.state === 'active');
417
- if (!rooms.length) {
418
- await ctx.reply('๐Ÿ  No rooms.');
419
- break;
420
- }
421
- const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
422
- await ctx.reply(tail(`๐Ÿ  Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
523
+ await ctx.reply(renderMarkdownList({
524
+ icon: '๐Ÿ ', title: 'Rooms', empty: 'No rooms found.',
525
+ records: rooms.map(roomListRecord),
526
+ }));
423
527
  break;
424
528
  }
425
529
  case 'show': {
@@ -428,12 +532,11 @@ export const ownerCommands = [
428
532
  await showRoom(rest[0]);
429
533
  break;
430
534
  }
535
+ case 'delete':
431
536
  case 'close': {
432
537
  if (rest.length < 2 || rest[0] !== rest[1])
433
- throw new OwnerCommandUsageError('destructive: /room close <id> <id> โ€” provide the room ID twice');
434
- const { closeRoom } = await import('../rooms-tasks/room-state.js');
435
- const r = closeRoom(rest[0]);
436
- await ctx.reply(`๐Ÿ”’ Room ${r.room_id} โ†’ closed`);
538
+ throw new OwnerCommandUsageError(`destructive: /room ${sub} <id> <id> โ€” provide the room ID twice`);
539
+ await ctx.closeRoom(rest[0]);
437
540
  break;
438
541
  }
439
542
  case 'recover': {
@@ -442,16 +545,29 @@ export const ownerCommands = [
442
545
  const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
443
546
  const r = getRoomRecord(rest[0]);
444
547
  if (!r) {
445
- await ctx.reply(`โš ๏ธ room not found: ${rest[0]}`);
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);
446
556
  break;
447
557
  }
448
- const lines = [`๐Ÿ  Room ${r.room_id} ยท ${r.state} ยท saga: ${r.saga.phase}`];
449
- if (r.saga.error)
450
- lines.push(`Error: ${r.saga.error}`);
451
- if (r.provisioning_detail)
452
- lines.push(`Detail: ${r.provisioning_detail}`);
453
- lines.push(r.saga.error ? 'Run `ours-fleet room recover` from CLI for full recovery.' : 'No recovery needed.');
454
- await ctx.reply(lines.join('\n'));
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
+ }));
455
571
  break;
456
572
  }
457
573
  default:
@@ -462,7 +578,13 @@ export const ownerCommands = [
462
578
  catch (e) {
463
579
  if (e instanceof OwnerCommandUsageError)
464
580
  throw e;
465
- await ctx.reply(`โš ๏ธ ${e instanceof Error ? e.message : String(e)}`);
581
+ const failure = ownerRoomFailure(e);
582
+ await ctx.reply(renderMarkdownFailure({
583
+ kind: failure.kind,
584
+ subject: `/room ${sub}`,
585
+ ...(failure.detail ? { detail: failure.detail } : {}),
586
+ action: failure.action,
587
+ }));
466
588
  }
467
589
  },
468
590
  },
@@ -561,7 +683,10 @@ export async function dispatchOwnerCommand(text, ctx) {
561
683
  }
562
684
  catch (error) {
563
685
  if (error instanceof OwnerCommandUsageError)
564
- return ctx.reply(ownerCommandHelp(error.message));
686
+ return ctx.reply(renderMarkdownFailure({
687
+ kind: 'usage', subject: token, detail: error.message,
688
+ action: `Use ${command.usage ?? `/${command.name}`} or run /help.`,
689
+ }));
565
690
  // The failure notice carries no internal detail; the channel logs it.
566
691
  await ctx.reply(ownerNotices.commandFailed(command.usage ?? `/${command.name}`));
567
692
  throw error;
@@ -583,5 +708,7 @@ export function fleetCliOps(role, configPath) {
583
708
  list: () => new Promise((resolve, reject) => {
584
709
  execFile(process.execPath, [process.argv[1], 'ls'], { timeout: 15_000, maxBuffer: 256 * 1024 }, (error, stdout) => error ? reject(error) : resolve(String(stdout).trim()));
585
710
  }),
711
+ closeRoom: roomId => launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath),
712
+ settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
586
713
  };
587
714
  }
@@ -22,6 +22,13 @@ export declare const ownerNotices: {
22
22
  receivedStarted: () => string;
23
23
  receivedQueued: (queuedBehind: number) => string;
24
24
  receivedInterrupting: () => string;
25
+ /**
26
+ * The honest answer when the agent is mid-task and pre-empting it would have
27
+ * corrupted the conversation. Says "not started yet" rather than borrowing
28
+ * `receivedInterrupting`'s claim that something was cancelled for this
29
+ * request.
30
+ */
31
+ receivedDeferred: () => string;
25
32
  status: (role: string, snapshot: SessionSnapshot) => string;
26
33
  interrupted: (role: string) => string;
27
34
  /** The turn IS cancelled โ€” say how, without implying the owner must retry. */
@@ -26,6 +26,15 @@ export const ownerNotices = {
26
26
  receivedInterrupting: () => "โ„น๏ธ Message received. The agent's previous task was interrupted to prioritize "
27
27
  + 'this request, and it is now working on a response. '
28
28
  + 'The response will arrive in this channel when ready.',
29
+ /**
30
+ * The honest answer when the agent is mid-task and pre-empting it would have
31
+ * corrupted the conversation. Says "not started yet" rather than borrowing
32
+ * `receivedInterrupting`'s claim that something was cancelled for this
33
+ * request.
34
+ */
35
+ receivedDeferred: () => 'โ„น๏ธ Message received and held. The agent is in the middle of a task that '
36
+ + 'cannot be interrupted safely; this request starts as soon as that work '
37
+ + 'reaches a stopping point. The response will arrive in this channel when ready.',
29
38
  status: (role, snapshot) => `๐Ÿ“Š ${role} status: ${snapshot.readiness}; session is ${snapshot.alive ? 'online' : 'offline'}.`,
30
39
  interrupted: (role) => `๐Ÿ›‘ Interrupt sent to ${role}'s active turn.`,
31
40
  /** The turn IS cancelled โ€” say how, without implying the owner must retry. */