@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.
- package/README.md +13 -9
- package/dist/application/errors.d.ts +1 -1
- package/dist/application/role-command-service.d.ts +29 -1
- package/dist/application/role-command-service.js +41 -2
- package/dist/application/role-creation-service.d.ts +32 -1
- package/dist/application/role-creation-service.js +56 -10
- package/dist/application/role-removal-service.d.ts +19 -0
- package/dist/application/role-removal-service.js +13 -3
- package/dist/application/session-mutations.d.ts +7 -0
- package/dist/application/session-mutations.js +8 -0
- package/dist/application/task-room-service.d.ts +262 -0
- package/dist/application/task-room-service.js +571 -0
- package/dist/atomic-file.d.ts +5 -3
- package/dist/atomic-file.js +32 -8
- package/dist/build-info.json +4 -4
- package/dist/cli.js +39 -15
- package/dist/config.d.ts +6 -3
- package/dist/config.js +0 -17
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +26 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/owner-channel/attachments.d.ts +2 -4
- package/dist/owner-channel/attachments.js +6 -39
- package/dist/owner-channel/channel.d.ts +5 -0
- package/dist/owner-channel/channel.js +159 -21
- package/dist/owner-channel/commands.d.ts +43 -1
- package/dist/owner-channel/commands.js +137 -130
- package/dist/rooms-tasks/cli.js +301 -463
- package/dist/rooms-tasks/task-lists.d.ts +19 -0
- package/dist/rooms-tasks/task-lists.js +96 -0
- package/dist/rooms-tasks/task-state.d.ts +3 -0
- package/dist/rooms-tasks/task-state.js +281 -175
- package/dist/rooms-tasks/types.d.ts +11 -1
- package/dist/runner.js +10 -33
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +22 -21
- package/dist/watchdog/query.d.ts +2 -0
- package/dist/watchdog/query.js +7 -3
- package/dist/web/runtime.js +2 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +88 -3
- 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 {
|
|
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
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
299
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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:
|
|
477
|
-
{ label: 'Name', value:
|
|
478
|
-
{ label: 'Status', value: roomStatus(
|
|
479
|
-
{ label: 'Saga', value: `${r.saga.phase} (step ${r.saga.step_index})` },
|
|
480
|
-
...(r
|
|
481
|
-
...(r
|
|
482
|
-
...(r
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
|
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
|
}
|