@ours.network/fleet 0.19.0-nightly.13 → 0.19.0-nightly.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/build-info.json +4 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +8 -0
- package/dist/owner-channel/channel.js +18 -3
- package/dist/owner-channel/commands.js +198 -87
- package/dist/rooms-tasks/cli.js +453 -192
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/package.json +1 -1
|
@@ -1,11 +1,92 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
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';
|
|
3
6
|
import { OWNER_COMMENT_LABEL, ownerNotices } from './notices.js';
|
|
4
7
|
/** A malformed invocation; the dispatcher answers it with annotated help. */
|
|
5
8
|
class OwnerCommandUsageError extends Error {
|
|
6
9
|
}
|
|
7
10
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
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
|
+
});
|
|
9
90
|
const strip = (value, max) => String(value).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, max);
|
|
10
91
|
/** Like `strip`, but keeps newlines so multi-line listings stay readable. */
|
|
11
92
|
const stripMultiline = (value, max) => String(value).replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, ' ').slice(0, max);
|
|
@@ -150,13 +231,10 @@ export const ownerCommands = [
|
|
|
150
231
|
const { listTasks } = await import('../rooms-tasks/task-state.js');
|
|
151
232
|
const stateFilter = args?.trim() || undefined;
|
|
152
233
|
const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return `${t.task_id} ${t.state}${blocked} ${t.title}`;
|
|
158
|
-
});
|
|
159
|
-
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
|
+
}));
|
|
160
238
|
},
|
|
161
239
|
},
|
|
162
240
|
{
|
|
@@ -174,16 +252,19 @@ export const ownerCommands = [
|
|
|
174
252
|
const showTask = async (id) => {
|
|
175
253
|
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
176
254
|
const t = getTask(id);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}));
|
|
187
268
|
};
|
|
188
269
|
try {
|
|
189
270
|
switch (sub) {
|
|
@@ -206,12 +287,11 @@ export const ownerCommands = [
|
|
|
206
287
|
...(noRoom ? { no_room: true } : {}),
|
|
207
288
|
...(trailingLines ? { brief: trailingLines } : {}),
|
|
208
289
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
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
|
+
]));
|
|
215
295
|
break;
|
|
216
296
|
}
|
|
217
297
|
case 'list': {
|
|
@@ -227,20 +307,16 @@ export const ownerCommands = [
|
|
|
227
307
|
const tasks = listTasks(stateFilter);
|
|
228
308
|
if (filter === 'blocked') {
|
|
229
309
|
const blocked = tasks.filter(t => t.blocked);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
const lines = blocked.map(t => `${t.task_id} ${t.state} ${t.title} [BLOCKED: ${t.blocked.reason}]`);
|
|
235
|
-
await ctx.reply(tail(`📋 Blocked tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
if (!tasks.length) {
|
|
239
|
-
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
|
+
}));
|
|
240
314
|
break;
|
|
241
315
|
}
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
await ctx.reply(renderMarkdownList({
|
|
317
|
+
icon: '📋', title: 'Tasks', empty: 'No tasks found.',
|
|
318
|
+
records: tasks.map(taskListRecord),
|
|
319
|
+
}));
|
|
244
320
|
break;
|
|
245
321
|
}
|
|
246
322
|
case 'show': {
|
|
@@ -254,7 +330,7 @@ export const ownerCommands = [
|
|
|
254
330
|
throw new OwnerCommandUsageError('usage: /task start <id>');
|
|
255
331
|
const { startTask } = await import('../rooms-tasks/task-state.js');
|
|
256
332
|
const t = startTask(rest[0]);
|
|
257
|
-
await ctx.reply(
|
|
333
|
+
await ctx.reply(taskAction('Task started', t));
|
|
258
334
|
break;
|
|
259
335
|
}
|
|
260
336
|
case 'block': {
|
|
@@ -263,7 +339,7 @@ export const ownerCommands = [
|
|
|
263
339
|
const { blockTask } = await import('../rooms-tasks/task-state.js');
|
|
264
340
|
const reason = rest.slice(1).join(' ');
|
|
265
341
|
const t = blockTask(rest[0], reason);
|
|
266
|
-
await ctx.reply(
|
|
342
|
+
await ctx.reply(taskAction('Task blocked', t, [{ label: 'Reason', value: reason }]));
|
|
267
343
|
break;
|
|
268
344
|
}
|
|
269
345
|
case 'unblock': {
|
|
@@ -271,7 +347,7 @@ export const ownerCommands = [
|
|
|
271
347
|
throw new OwnerCommandUsageError('usage: /task unblock <id>');
|
|
272
348
|
const { unblockTask } = await import('../rooms-tasks/task-state.js');
|
|
273
349
|
const t = unblockTask(rest[0]);
|
|
274
|
-
await ctx.reply(
|
|
350
|
+
await ctx.reply(taskAction('Task unblocked', t));
|
|
275
351
|
break;
|
|
276
352
|
}
|
|
277
353
|
case 'review': {
|
|
@@ -279,7 +355,7 @@ export const ownerCommands = [
|
|
|
279
355
|
throw new OwnerCommandUsageError('usage: /task review <id>');
|
|
280
356
|
const { reviewTask } = await import('../rooms-tasks/task-state.js');
|
|
281
357
|
const t = reviewTask(rest[0]);
|
|
282
|
-
await ctx.reply(
|
|
358
|
+
await ctx.reply(taskAction('Task ready for review', t));
|
|
283
359
|
break;
|
|
284
360
|
}
|
|
285
361
|
case 'done': {
|
|
@@ -300,9 +376,10 @@ export const ownerCommands = [
|
|
|
300
376
|
throw new OwnerCommandUsageError('destructive: /task delete <id> <id> — provide the task ID twice');
|
|
301
377
|
const { deleteTask } = await import('../rooms-tasks/task-state.js');
|
|
302
378
|
const deleted = deleteTask(rest[0]);
|
|
303
|
-
await ctx.reply(
|
|
304
|
-
?
|
|
305
|
-
:
|
|
379
|
+
await ctx.reply(renderMarkdownResult({
|
|
380
|
+
icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
|
|
381
|
+
fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
|
|
382
|
+
}));
|
|
306
383
|
break;
|
|
307
384
|
}
|
|
308
385
|
case 'recover': {
|
|
@@ -327,14 +404,21 @@ export const ownerCommands = [
|
|
|
327
404
|
if (room.provisioning_detail === 'member_failed')
|
|
328
405
|
hints.push(`Member failed at step ${room.saga.step_index}`);
|
|
329
406
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}));
|
|
338
422
|
break;
|
|
339
423
|
}
|
|
340
424
|
default:
|
|
@@ -345,7 +429,13 @@ export const ownerCommands = [
|
|
|
345
429
|
catch (e) {
|
|
346
430
|
if (e instanceof OwnerCommandUsageError)
|
|
347
431
|
throw e;
|
|
348
|
-
|
|
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
|
+
}));
|
|
349
439
|
}
|
|
350
440
|
},
|
|
351
441
|
},
|
|
@@ -354,10 +444,10 @@ export const ownerCommands = [
|
|
|
354
444
|
execute: noArgs('/rooms', async (ctx) => {
|
|
355
445
|
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
356
446
|
const rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
447
|
+
await ctx.reply(renderMarkdownList({
|
|
448
|
+
icon: '🏠', title: 'Rooms', empty: 'No rooms found.',
|
|
449
|
+
records: rooms.map(roomListRecord),
|
|
450
|
+
}));
|
|
361
451
|
}),
|
|
362
452
|
},
|
|
363
453
|
{
|
|
@@ -376,18 +466,23 @@ export const ownerCommands = [
|
|
|
376
466
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
377
467
|
const r = getRoomRecord(id);
|
|
378
468
|
if (!r || r.state === 'closing' || r.state === 'closed')
|
|
379
|
-
return ctx.reply(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}));
|
|
391
486
|
};
|
|
392
487
|
try {
|
|
393
488
|
switch (sub) {
|
|
@@ -413,10 +508,10 @@ export const ownerCommands = [
|
|
|
413
508
|
...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
|
|
414
509
|
...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
|
|
415
510
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
511
|
+
await ctx.reply(roomAction('Room created', r, [
|
|
512
|
+
{ label: 'Name', value: r.room_name },
|
|
513
|
+
...(templateName ? [{ label: 'Template', value: templateName, kind: 'code' }] : []),
|
|
514
|
+
]));
|
|
420
515
|
break;
|
|
421
516
|
}
|
|
422
517
|
case 'list': {
|
|
@@ -425,12 +520,10 @@ export const ownerCommands = [
|
|
|
425
520
|
let rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
|
|
426
521
|
if (filter === 'active')
|
|
427
522
|
rooms = rooms.filter(r => r.state === 'active');
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
433
|
-
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
|
+
}));
|
|
434
527
|
break;
|
|
435
528
|
}
|
|
436
529
|
case 'show': {
|
|
@@ -452,20 +545,29 @@ export const ownerCommands = [
|
|
|
452
545
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
453
546
|
const r = getRoomRecord(rest[0]);
|
|
454
547
|
if (!r) {
|
|
455
|
-
await ctx.reply(
|
|
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
|
+
}));
|
|
456
552
|
break;
|
|
457
553
|
}
|
|
458
554
|
if (r.state === 'closing' || r.state === 'closed') {
|
|
459
555
|
await ctx.closeRoom(r.room_id);
|
|
460
556
|
break;
|
|
461
557
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
}));
|
|
469
571
|
break;
|
|
470
572
|
}
|
|
471
573
|
default:
|
|
@@ -476,7 +578,13 @@ export const ownerCommands = [
|
|
|
476
578
|
catch (e) {
|
|
477
579
|
if (e instanceof OwnerCommandUsageError)
|
|
478
580
|
throw e;
|
|
479
|
-
|
|
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
|
+
}));
|
|
480
588
|
}
|
|
481
589
|
},
|
|
482
590
|
},
|
|
@@ -575,7 +683,10 @@ export async function dispatchOwnerCommand(text, ctx) {
|
|
|
575
683
|
}
|
|
576
684
|
catch (error) {
|
|
577
685
|
if (error instanceof OwnerCommandUsageError)
|
|
578
|
-
return ctx.reply(
|
|
686
|
+
return ctx.reply(renderMarkdownFailure({
|
|
687
|
+
kind: 'usage', subject: token, detail: error.message,
|
|
688
|
+
action: `Use ${command.usage ?? `/${command.name}`} or run /help.`,
|
|
689
|
+
}));
|
|
579
690
|
// The failure notice carries no internal detail; the channel logs it.
|
|
580
691
|
await ctx.reply(ownerNotices.commandFailed(command.usage ?? `/${command.name}`));
|
|
581
692
|
throw error;
|