@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.
@@ -9,11 +9,171 @@ import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unb
9
9
  import { createRoomRecord, getRoomRecord, listRoomRecords, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
10
10
  import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
11
11
  import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from './types.js';
12
+ import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
13
+ class TaskRoomPublicError extends Error {
14
+ code;
15
+ fields;
16
+ constructor(code, fields = {}) {
17
+ super(code);
18
+ this.code = code;
19
+ this.fields = fields;
20
+ this.name = 'TaskRoomPublicError';
21
+ }
22
+ }
23
+ const PUBLIC_ERROR_FIELD = /^[A-Za-z0-9._:@-]{1,160}$/u;
24
+ function taskRoomPublicError(code, fields = {}) {
25
+ const validated = Object.fromEntries(Object.entries(fields).flatMap(([key, value]) => {
26
+ const text = String(value ?? '');
27
+ return PUBLIC_ERROR_FIELD.test(text) ? [[key, text]] : [];
28
+ }));
29
+ return new TaskRoomPublicError(code, validated);
30
+ }
31
+ function taskRoomPublicFailure(error) {
32
+ const f = error.fields;
33
+ switch (error.code) {
34
+ case 'task_confirmation_mismatch': return {
35
+ legacy: 'confirmation ID must match task ID', kind: 'usage',
36
+ detail: 'The two task IDs must match.', action: 'Repeat the same task ID twice.',
37
+ };
38
+ case 'room_confirmation_mismatch': return {
39
+ legacy: 'confirmation ID must match room ID', kind: 'usage',
40
+ detail: 'The two room IDs must match.', action: 'Repeat the same room ID twice.',
41
+ };
42
+ case 'template_not_found': return {
43
+ legacy: `template not found: ${f.template ?? 'requested-template'}`, kind: 'not_found',
44
+ detail: `The requested template ${f.template ?? ''}`.trim() + ' was not found.',
45
+ action: 'Run ours-fleet template list and retry with an available template.',
46
+ };
47
+ case 'template_mismatch': return {
48
+ legacy: `template ${f.requested ?? 'requested-template'} does not match room ${f.room ?? 'requested-room'}'s provisioned template ${f.provisioned ?? 'recorded-template'}`,
49
+ kind: 'state', detail: 'The requested template does not match the room’s provisioned template.',
50
+ action: 'Use the room’s recorded template or create a new room.',
51
+ };
52
+ case 'task_template_drift': return {
53
+ legacy: `task template snapshot no longer matches ${f.template ?? 'the recorded template'}`,
54
+ kind: 'state', detail: 'The task template snapshot no longer matches the recorded template.',
55
+ action: 'Run the matching show and recover commands before retrying.',
56
+ };
57
+ case 'task_terminal': return {
58
+ legacy: `task ${f.task ?? 'requested-task'} is in terminal state '${f.state ?? 'terminal'}'`,
59
+ kind: 'state', detail: 'The task is already in a terminal state.',
60
+ action: 'Run ours-fleet task show to inspect the completed task.',
61
+ };
62
+ case 'task_terminal_already': return {
63
+ legacy: `task ${f.task ?? 'requested-task'} is already in terminal state '${f.state ?? 'terminal'}'`,
64
+ kind: 'state', detail: 'The task is already in a terminal state.',
65
+ action: 'Run ours-fleet task show to inspect the completed task.',
66
+ };
67
+ case 'task_non_resumable': return {
68
+ legacy: `task ${f.task ?? 'requested-task'} has room ${f.room ?? 'requested-room'} in a non-resumable state — run 'task recover ${f.task ?? 'requested-task'}'`,
69
+ kind: 'state', detail: 'The task’s room is in a non-resumable state.',
70
+ action: `Run ours-fleet task recover ${f.task ?? '<id>'}.`,
71
+ };
72
+ case 'room_filter': return {
73
+ legacy: 'room state filter must be active, provisioning, or all', kind: 'usage',
74
+ detail: 'The room state filter must be active, provisioning, or all.',
75
+ action: 'Choose one of the supported room state filters.',
76
+ };
77
+ case 'room_not_found': return {
78
+ legacy: `room not found: ${f.room ?? 'requested-room'}`, kind: 'not_found',
79
+ detail: `Room ${f.room ?? ''}`.trim() + ' was not found.',
80
+ action: 'Run ours-fleet room list to find a live room ID.',
81
+ };
82
+ case 'room_record_not_found': return {
83
+ legacy: `room not found in Fleet orchestration: ${f.room ?? 'requested-room'}`, kind: 'not_found',
84
+ detail: `Room ${f.room ?? ''}`.trim() + ' was not found in Fleet orchestration.',
85
+ action: 'Run ours-fleet room list to find a live room ID.',
86
+ };
87
+ }
88
+ }
12
89
  function die(e) {
13
- const msg = e instanceof Error ? e.message : String(e);
90
+ const msg = e instanceof TaskRoomPublicError
91
+ ? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
14
92
  process.stderr.write(`error: ${msg}\n`);
15
93
  process.exit(1);
16
94
  }
95
+ const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
96
+ const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
97
+ function isKnownTaskStateMessage(message) {
98
+ return [
99
+ new RegExp(`^cannot transition from '${TASK_STATE_WORD}' to '${TASK_STATE_WORD}'$`, 'u'),
100
+ new RegExp(`^task ${SAFE_ID_WORD} has a pending '(?:done|cancelled)' terminal intent$`, 'u'),
101
+ new RegExp(`^cannot (?:block|cancel) a '${TASK_STATE_WORD}' task$`, 'u'),
102
+ /^task is not blocked$/u,
103
+ new RegExp(`^task ${SAFE_ID_WORD} already has a conflicting '(?:done|cancelled)' terminal intent$`, 'u'),
104
+ new RegExp(`^task ${SAFE_ID_WORD} is already in terminal state '${TASK_STATE_WORD}'$`, 'u'),
105
+ new RegExp(`^task ${SAFE_ID_WORD} is not tied to room ${SAFE_ID_WORD}$`, 'u'),
106
+ new RegExp(`^task ${SAFE_ID_WORD} has no terminal intent$`, 'u'),
107
+ new RegExp(`^task ${SAFE_ID_WORD} reached terminal state '${TASK_STATE_WORD}' outside its intent$`, 'u'),
108
+ new RegExp(`^cannot delete a '${TASK_STATE_WORD}' task; only 'done' tasks can be deleted$`, 'u'),
109
+ ].some(pattern => pattern.test(message));
110
+ }
111
+ function isKnownRoomStateMessage(message) {
112
+ return [
113
+ new RegExp(`^room ${SAFE_ID_WORD} history cursor cannot move backward$`, 'u'),
114
+ new RegExp(`^room ${SAFE_ID_WORD} is not closing$`, 'u'),
115
+ new RegExp(`^room ${SAFE_ID_WORD} has no recorded member .{1,128}$`, 'u'),
116
+ new RegExp(`^room ${SAFE_ID_WORD} member .{1,128} (?:launch|briefing|retirement) cannot move backward to [A-Za-z_]+$`, 'u'),
117
+ new RegExp(`^room ${SAFE_ID_WORD} member .{1,128} launch changed from ${SAFE_ID_WORD} to ${SAFE_ID_WORD}$`, 'u'),
118
+ new RegExp(`^room ${SAFE_ID_WORD} close cannot move backward to [A-Za-z_]+$`, 'u'),
119
+ ].some(pattern => pattern.test(message));
120
+ }
121
+ function dieTaskRoom(e) {
122
+ if (e instanceof TaskRoomPublicError) {
123
+ const failure = taskRoomPublicFailure(e);
124
+ const output = renderMarkdownFailure({
125
+ kind: failure.kind, subject: 'ours-fleet task/room', detail: failure.detail,
126
+ action: failure.action,
127
+ });
128
+ process.stderr.write(`${output}\n`);
129
+ process.exit(1);
130
+ }
131
+ const taskMissing = e instanceof TaskStateError
132
+ ? /^task not found: ([A-Za-z0-9_-]{1,128})$/u.exec(e.message) : null;
133
+ const roomMissing = e instanceof RoomStateError
134
+ ? /^room not found: ([A-Za-z0-9_-]{1,128})$/u.exec(e.message) : null;
135
+ const notFoundId = taskMissing?.[1] ?? roomMissing?.[1];
136
+ const state = e instanceof TaskStateError ? isKnownTaskStateMessage(e.message)
137
+ : e instanceof RoomStateError ? isKnownRoomStateMessage(e.message) : false;
138
+ const validation = e instanceof ConfigError
139
+ || (e instanceof TaskStateError
140
+ && e.message === 'invalid task ID: expected the canonical 17-character lowercase ID');
141
+ const kind = notFoundId ? 'not_found' : state ? 'state' : validation ? 'usage' : 'unexpected';
142
+ const output = renderMarkdownFailure({
143
+ kind,
144
+ subject: 'ours-fleet task/room',
145
+ ...(notFoundId ? { detail: `${taskMissing ? 'Task' : 'Room'} ${notFoundId} was not found.` }
146
+ : state ? { detail: 'The current task or room state does not allow that action.' }
147
+ : validation ? { detail: 'Fleet configuration is invalid or incomplete.' } : {}),
148
+ action: notFoundId ? 'Run the matching list command to find a valid ID.'
149
+ : state ? 'Run the matching show command to inspect the current state.'
150
+ : validation ? 'Check the command options and configuration, then retry.'
151
+ : 'Retry once; if it repeats, run ours-fleet doctor and inspect the role logs.',
152
+ });
153
+ process.stderr.write(`${output}\n`);
154
+ process.exit(1);
155
+ }
156
+ const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
157
+ icon: '📋', title, empty: 'No tasks found.',
158
+ records: tasks.map(t => `${taskStatus(t.state)} ${markdownCode(t.task_id)} — ${markdownProse(t.title)}`
159
+ + (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
160
+ });
161
+ const taskActionMarkdown = (title, task, fields = []) => renderMarkdownResult({
162
+ icon: '📋', title,
163
+ fields: [
164
+ { label: 'ID', value: task.task_id, kind: 'code' },
165
+ { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
166
+ ...fields,
167
+ ],
168
+ });
169
+ const roomActionMarkdown = (title, room, fields = []) => renderMarkdownResult({
170
+ icon: '🏠', title,
171
+ fields: [
172
+ { label: 'ID', value: room.room_id, kind: 'code' },
173
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
174
+ ...fields,
175
+ ],
176
+ });
17
177
  const PUBLIC_SETTLE_WAIT_MS = 60_000;
18
178
  const PUBLIC_SETTLE_POLL_MS = 100;
19
179
  const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
@@ -92,27 +252,31 @@ function durableTaskRoomTemplate(task, room) {
92
252
  }
93
253
  return snapshot;
94
254
  }
95
- function printRoomStartupEvidence(room) {
255
+ function roomStartupSections(room) {
256
+ const sections = [];
96
257
  const definitions = Object.values(room.role_briefings ?? {});
97
258
  if (definitions.length) {
98
- console.log('Role briefings:');
99
- for (const definition of definitions.sort((a, b) => a.role.localeCompare(b.role)))
100
- console.log(` ${definition.role} v${definition.version ?? '?'} ${definition.state} sha256:${definition.sha256}`);
259
+ sections.push({
260
+ heading: 'Role briefings',
261
+ markdownItems: definitions.sort((a, b) => a.role.localeCompare(b.role)).map(definition => `${markdownCode(definition.role)} — version ${markdownCode(definition.version ?? '?')} — ${markdownProse(definition.state)}`),
262
+ });
101
263
  }
102
264
  if (room.member_seats.length) {
103
- console.log('Fleet startup evidence:');
104
- for (const seat of room.member_seats) {
105
- const launch = seat.launch?.state ?? 'unrecorded';
106
- const briefing = seat.briefing?.state ?? 'unrecorded';
107
- console.log(` ${seat.role_name} ${seat.identity_cid} role=${seat.cowork_role} seat=${seat.seat_state} launch=${launch} briefing=${briefing}`);
108
- if (seat.briefing?.message_id)
109
- console.log(` briefing_message=${seat.briefing.message_id} relay=${seat.briefing.relay_result_record_id ?? 'pending'} ack=${seat.briefing.acknowledgement_message_id ?? 'pending'}`);
110
- if (seat.briefing?.last_rejected_ack_reason)
111
- console.log(` rejected_ack_seq=${seat.briefing.last_rejected_ack_seq} reason=${seat.briefing.last_rejected_ack_reason}`);
112
- if (seat.launch?.error)
113
- console.log(` launch_error=${seat.launch.error}`);
114
- }
265
+ sections.push({
266
+ heading: 'Fleet startup evidence',
267
+ markdownItems: room.member_seats.map(seat => {
268
+ const launch = seat.launch?.state ?? 'unrecorded';
269
+ const briefing = seat.briefing?.state ?? 'unrecorded';
270
+ return `${markdownCode(seat.role_name)} — ${markdownProse(seat.cowork_role)} — `
271
+ + `seat ${markdownProse(seat.seat_state)}, launch ${markdownProse(launch)}, briefing ${markdownProse(briefing)}`
272
+ + (seat.briefing?.acknowledgement_message_id ? ' — acknowledgement recorded' : '')
273
+ + (seat.briefing?.last_rejected_ack_reason
274
+ ? ' — rejected acknowledgement recorded; inspect role logs' : '')
275
+ + (seat.launch?.error ? ' — launch failure recorded; inspect role logs' : '');
276
+ }),
277
+ });
115
278
  }
279
+ return sections;
116
280
  }
117
281
  async function provisionRoom(cfg, input) {
118
282
  const rooms = cfg.rooms;
@@ -284,7 +448,7 @@ export function registerTaskCommands(parent, cOpt) {
284
448
  if (opts.template) {
285
449
  const t = resolveTemplate(opts.template, allTemplates(cfg));
286
450
  if (!t)
287
- die(new Error(`template not found: ${opts.template}`));
451
+ throw taskRoomPublicError('template_not_found', { template: opts.template });
288
452
  const snap = snapshotTemplate(t);
289
453
  templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
290
454
  }
@@ -336,12 +500,15 @@ export function registerTaskCommands(parent, cOpt) {
336
500
  console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
337
501
  return;
338
502
  }
339
- console.log(`Task ${record.task_id} created · ${record.state}`);
340
- if (templateRef)
341
- console.log(`Template: ${templateRef.name}@${templateRef.version}`);
503
+ console.log(taskActionMarkdown('Task created', record, [
504
+ { label: 'Title', value: record.title },
505
+ ...(templateRef ? [{ label: 'Template', value: `${templateRef.name}@${templateRef.version}`, kind: 'code' }] : []),
506
+ ]));
342
507
  }
343
508
  catch (e) {
344
- die(e);
509
+ if (opts.json)
510
+ die(e);
511
+ dieTaskRoom(e);
345
512
  }
346
513
  });
347
514
  cOpt(taskCmd.command('list'))
@@ -359,17 +526,12 @@ export function registerTaskCommands(parent, cOpt) {
359
526
  console.log(JSON.stringify({ schema_version: 1, tasks }, null, 2));
360
527
  return;
361
528
  }
362
- if (!tasks.length) {
363
- console.log('No tasks.');
364
- return;
365
- }
366
- for (const t of tasks) {
367
- const blocked = t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : '';
368
- console.log(`${t.task_id} ${t.state}${blocked} ${t.title}`);
369
- }
529
+ console.log(taskListMarkdown(tasks));
370
530
  }
371
531
  catch (e) {
372
- die(e);
532
+ if (opts.json)
533
+ die(e);
534
+ dieTaskRoom(e);
373
535
  }
374
536
  });
375
537
  taskCmd.command('show <id>')
@@ -385,33 +547,34 @@ export function registerTaskCommands(parent, cOpt) {
385
547
  }, null, 2));
386
548
  return;
387
549
  }
388
- console.log(`Task: ${t.task_id}`);
389
- console.log(`Title: ${t.title}`);
390
- console.log(`State: ${t.state}${t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : ''}`);
391
- if (t.template)
392
- console.log(`Template: ${t.template.name}@${t.template.version}`);
393
- if (t.room_id)
394
- console.log(`Room: ${t.room_id}`);
395
- if (t.room_identity_cid)
396
- console.log(`Room CID: ${t.room_identity_cid}`);
397
- if (t.member_roles.length) {
398
- console.log('Members:');
399
- for (const m of t.member_roles)
400
- console.log(` ${m.name} (${m.cowork_role}) ${m.identity_cid}`);
401
- }
402
- if (room)
403
- printRoomStartupEvidence(room);
404
- console.log(`Origin: ${t.origin.type}`);
405
- console.log(`Created: ${t.created_at}`);
406
- if (t.started_at)
407
- console.log(`Started: ${t.started_at}`);
408
- if (t.ended_at)
409
- console.log(`Ended: ${t.ended_at}`);
410
- if (t.outcome)
411
- console.log(`Outcome: ${t.outcome.summary}`);
550
+ console.log(renderMarkdownResult({
551
+ icon: '📋', title: 'Task details',
552
+ fields: [
553
+ { label: 'ID', value: t.task_id, kind: 'code' },
554
+ { label: 'Title', value: t.title },
555
+ { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
556
+ ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
557
+ ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
558
+ ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
559
+ ...(t.room_identity_cid ? [{ label: 'Room CID', value: t.room_identity_cid, kind: 'code' }] : []),
560
+ { label: 'Origin', value: t.origin.type },
561
+ { label: 'Created', value: t.created_at, kind: 'code' },
562
+ ...(t.started_at ? [{ label: 'Started', value: t.started_at, kind: 'code' }] : []),
563
+ ...(t.ended_at ? [{ label: 'Ended', value: t.ended_at, kind: 'code' }] : []),
564
+ ...(t.outcome ? [{ label: 'Outcome', value: t.outcome.summary, multiline: true }] : []),
565
+ ],
566
+ sections: [
567
+ ...(t.member_roles.length ? [{
568
+ heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`),
569
+ }] : []),
570
+ ...(room ? roomStartupSections(room) : []),
571
+ ],
572
+ }));
412
573
  }
413
574
  catch (e) {
414
- die(e);
575
+ if (opts.json)
576
+ die(e);
577
+ dieTaskRoom(e);
415
578
  }
416
579
  });
417
580
  cOpt(taskCmd.command('start <id>'))
@@ -424,7 +587,9 @@ export function registerTaskCommands(parent, cOpt) {
424
587
  if (t.template && !t.room_id) {
425
588
  const template = resolveRoomTemplate(cfg, t.template.name);
426
589
  if (!template || template.content_hash !== t.template.content_hash)
427
- throw new Error(`task template snapshot no longer matches ${t.template.name}@${t.template.version}`);
590
+ throw taskRoomPublicError('task_template_drift', {
591
+ template: `${t.template.name}@${t.template.version}`,
592
+ });
428
593
  await provisionRoom(cfg, {
429
594
  name: t.title,
430
595
  goal: t.title,
@@ -441,10 +606,12 @@ export function registerTaskCommands(parent, cOpt) {
441
606
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
442
607
  return;
443
608
  }
444
- console.log(`Task ${t.task_id} · provisioning`);
609
+ console.log(taskActionMarkdown('Task started', t));
445
610
  }
446
611
  catch (e) {
447
- die(e);
612
+ if (opts.json)
613
+ die(e);
614
+ dieTaskRoom(e);
448
615
  }
449
616
  });
450
617
  taskCmd.command('block <id>')
@@ -458,10 +625,12 @@ export function registerTaskCommands(parent, cOpt) {
458
625
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
459
626
  return;
460
627
  }
461
- console.log(`Task ${t.task_id} · blocked: ${opts.reason}`);
628
+ console.log(taskActionMarkdown('Task blocked', t, [{ label: 'Reason', value: opts.reason }]));
462
629
  }
463
630
  catch (e) {
464
- die(e);
631
+ if (opts.json)
632
+ die(e);
633
+ dieTaskRoom(e);
465
634
  }
466
635
  });
467
636
  taskCmd.command('unblock <id>')
@@ -474,10 +643,12 @@ export function registerTaskCommands(parent, cOpt) {
474
643
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
475
644
  return;
476
645
  }
477
- console.log(`Task ${t.task_id} · unblocked`);
646
+ console.log(taskActionMarkdown('Task unblocked', t));
478
647
  }
479
648
  catch (e) {
480
- die(e);
649
+ if (opts.json)
650
+ die(e);
651
+ dieTaskRoom(e);
481
652
  }
482
653
  });
483
654
  taskCmd.command('review <id>')
@@ -490,10 +661,12 @@ export function registerTaskCommands(parent, cOpt) {
490
661
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
491
662
  return;
492
663
  }
493
- console.log(`Task ${t.task_id} · review`);
664
+ console.log(taskActionMarkdown('Task ready for review', t));
494
665
  }
495
666
  catch (e) {
496
- die(e);
667
+ if (opts.json)
668
+ die(e);
669
+ dieTaskRoom(e);
497
670
  }
498
671
  });
499
672
  cOpt(taskCmd.command('done <id>'))
@@ -532,13 +705,19 @@ export function registerTaskCommands(parent, cOpt) {
532
705
  return;
533
706
  }
534
707
  if (settled.timedOut) {
535
- console.log(`Task ${t.task_id} · ${t.state} (terminal intent accepted/pending; run task recover ${id})`);
708
+ console.log(renderMarkdownFailure({
709
+ kind: 'pending', subject: `task done ${id}`,
710
+ detail: 'The completion request was accepted and is still being settled.',
711
+ action: `Run ours-fleet task recover ${id}.`,
712
+ }));
536
713
  return;
537
714
  }
538
- console.log(`Task ${t.task_id} · done`);
715
+ console.log(taskActionMarkdown('Task completed', t, t.outcome ? [{ label: 'Summary', value: t.outcome.summary, multiline: true }] : []));
539
716
  }
540
717
  catch (e) {
541
- die(e);
718
+ if (opts.json)
719
+ die(e);
720
+ dieTaskRoom(e);
542
721
  }
543
722
  });
544
723
  cOpt(taskCmd.command('cancel <id> <confirm-id>'))
@@ -547,7 +726,7 @@ export function registerTaskCommands(parent, cOpt) {
547
726
  .action(async (id, confirmId, opts) => {
548
727
  try {
549
728
  if (id !== confirmId)
550
- die(new Error('confirmation ID must match task ID'));
729
+ throw taskRoomPublicError('task_confirmation_mismatch');
551
730
  const current = getTask(id);
552
731
  if (!TASK_CANCELLABLE_STATES.includes(current.state))
553
732
  throw new TaskStateError(`cannot cancel a '${current.state}' task`);
@@ -565,13 +744,19 @@ export function registerTaskCommands(parent, cOpt) {
565
744
  return;
566
745
  }
567
746
  if (settled.timedOut) {
568
- console.log(`Task ${t.task_id} · ${t.state} (terminal intent accepted/pending; run task recover ${id})`);
747
+ console.log(renderMarkdownFailure({
748
+ kind: 'pending', subject: `task cancel ${id} ${id}`,
749
+ detail: 'The cancellation request was accepted and is still being settled.',
750
+ action: `Run ours-fleet task recover ${id}.`,
751
+ }));
569
752
  return;
570
753
  }
571
- console.log(`Task ${t.task_id} · cancelled`);
754
+ console.log(taskActionMarkdown('Task cancelled', t));
572
755
  }
573
756
  catch (e) {
574
- die(e);
757
+ if (opts.json)
758
+ die(e);
759
+ dieTaskRoom(e);
575
760
  }
576
761
  });
577
762
  cOpt(taskCmd.command('delete <id> <confirm-id>'))
@@ -580,18 +765,21 @@ export function registerTaskCommands(parent, cOpt) {
580
765
  .action((id, confirmId, opts) => {
581
766
  try {
582
767
  if (id !== confirmId)
583
- die(new Error('confirmation ID must match task ID'));
768
+ throw taskRoomPublicError('task_confirmation_mismatch');
584
769
  const deleted = deleteTask(id);
585
770
  if (opts.json) {
586
771
  console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
587
772
  return;
588
773
  }
589
- console.log(deleted
590
- ? `Task ${id} · deleted from backlog`
591
- : `Task ${id} · already absent`);
774
+ console.log(renderMarkdownResult({
775
+ icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
776
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
777
+ }));
592
778
  }
593
779
  catch (e) {
594
- die(e);
780
+ if (opts.json)
781
+ die(e);
782
+ dieTaskRoom(e);
595
783
  }
596
784
  });
597
785
  cOpt(taskCmd.command('recover <id>'))
@@ -661,20 +849,26 @@ export function registerTaskCommands(parent, cOpt) {
661
849
  console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
662
850
  return;
663
851
  }
664
- console.log(`Task ${t.task_id} · ${t.state}`);
665
- if (room)
666
- console.log(`Room ${room.room_id} · ${room.state} · saga: ${room.saga.phase}`);
667
- if (result.recovery_actions.length) {
668
- console.log('Recovery actions:');
669
- for (const a of result.recovery_actions)
670
- console.log(` - ${a}`);
671
- }
672
- else {
673
- console.log('No automated recovery actions available.');
674
- }
852
+ console.log(renderMarkdownResult({
853
+ icon: '🛟', title: 'Task recovery',
854
+ fields: [
855
+ { label: 'Task', value: t.task_id, kind: 'code' },
856
+ { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
857
+ ...(room ? [
858
+ { label: 'Room', value: room.room_id, kind: 'code' },
859
+ { label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
860
+ { label: 'Saga', value: room.saga.phase, kind: 'code' },
861
+ ] : []),
862
+ ],
863
+ sections: result.recovery_actions.length
864
+ ? [{ heading: 'Next steps', items: result.recovery_actions }]
865
+ : [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
866
+ }));
675
867
  }
676
868
  catch (e) {
677
- die(e);
869
+ if (opts.json)
870
+ die(e);
871
+ dieTaskRoom(e);
678
872
  }
679
873
  });
680
874
  cOpt(taskCmd.command('_settle <id>', { hidden: true }))
@@ -688,11 +882,13 @@ export function registerTaskCommands(parent, cOpt) {
688
882
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
689
883
  return;
690
884
  }
691
- console.log(`Task ${t.task_id} · ${t.state}`);
885
+ console.log(taskActionMarkdown('Task terminal action settled', t));
692
886
  }
693
887
  catch (e) {
694
888
  await recordTaskTerminalIntentError(id, errorText(e), `External settle worker failed. Retry task recover ${id}.`).catch(() => { });
695
- die(e);
889
+ if (opts.json)
890
+ die(e);
891
+ dieTaskRoom(e);
696
892
  }
697
893
  });
698
894
  cOpt(taskCmd.command('work <id>'))
@@ -704,24 +900,27 @@ export function registerTaskCommands(parent, cOpt) {
704
900
  const cfg = loadCfg(opts);
705
901
  let t = getTask(id);
706
902
  if (TASK_TERMINAL_STATES.includes(t.state))
707
- die(new Error(`task ${id} is in terminal state '${t.state}'`));
903
+ throw taskRoomPublicError('task_terminal', { task: id, state: t.state });
708
904
  if (t.state === 'active' && t.room_id) {
709
905
  // An explicit --template must agree with the room the task already
710
906
  // runs in; a conflicting override is an error, not a silent no-op.
711
907
  if (opts.template) {
712
908
  const override = resolveTemplate(opts.template, allTemplates(cfg));
713
909
  if (!override)
714
- die(new Error(`template not found: ${opts.template}`));
910
+ throw taskRoomPublicError('template_not_found', { template: opts.template });
715
911
  const overrideSnap = snapshotTemplate(override);
716
912
  const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
717
913
  if (roomSnap && (roomSnap.name !== overrideSnap.name || roomSnap.content_hash !== overrideSnap.content_hash))
718
- die(new Error(`template ${overrideSnap.name}@${overrideSnap.version} does not match room ${t.room_id}'s provisioned template ${roomSnap.name}@${roomSnap.version}`));
914
+ throw taskRoomPublicError('template_mismatch', {
915
+ requested: `${overrideSnap.name}@${overrideSnap.version}`, room: t.room_id,
916
+ provisioned: `${roomSnap.name}@${roomSnap.version}`,
917
+ });
719
918
  }
720
919
  if (opts.json) {
721
920
  console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
722
921
  return;
723
922
  }
724
- console.log('Task already has an active room');
923
+ console.log(taskActionMarkdown('Task already active', t, t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []));
725
924
  return;
726
925
  }
727
926
  // Select and validate the template snapshot before any state change or
@@ -738,25 +937,33 @@ export function registerTaskCommands(parent, cOpt) {
738
937
  const resolved = durableSnapshot && !opts.template
739
938
  ? undefined : resolveTemplate(templateName, allTemplates(cfg));
740
939
  if (!durableSnapshot && !resolved)
741
- die(new Error(`template not found: ${templateName}`));
940
+ throw taskRoomPublicError('template_not_found', { template: templateName });
742
941
  if (opts.template && !resolved)
743
- die(new Error(`template not found: ${templateName}`));
942
+ throw taskRoomPublicError('template_not_found', { template: templateName });
744
943
  if (durableSnapshot && resolved) {
745
944
  const requested = snapshotTemplate(resolved);
746
945
  if (requested.name !== durableSnapshot.name
747
946
  || requested.content_hash !== durableSnapshot.content_hash) {
748
- die(new Error(`template ${requested.name}@${requested.version} does not match room ${existingRoom.room_id}'s provisioned template ${durableSnapshot.name}@${durableSnapshot.version}`));
947
+ throw taskRoomPublicError('template_mismatch', {
948
+ requested: `${requested.name}@${requested.version}`, room: existingRoom.room_id,
949
+ provisioned: `${durableSnapshot.name}@${durableSnapshot.version}`,
950
+ });
749
951
  }
750
952
  }
751
953
  const snap = durableSnapshot ?? snapshotTemplate(resolved);
752
954
  if (!opts.template && t.template && snap.content_hash !== t.template.content_hash)
753
- die(new Error(`task template snapshot no longer matches ${t.template.name}@${t.template.version}`));
955
+ throw taskRoomPublicError('task_template_drift', {
956
+ template: `${t.template.name}@${t.template.version}`,
957
+ });
754
958
  // A provisioned room is pinned to its snapshot: never resume or re-pin
755
959
  // an existing room under a different template.
756
960
  if (t.room_id) {
757
961
  const roomSnap = getRoomRecord(t.room_id)?.template_snapshot;
758
962
  if (roomSnap && (roomSnap.name !== snap.name || roomSnap.content_hash !== snap.content_hash))
759
- die(new Error(`template ${snap.name}@${snap.version} does not match room ${t.room_id}'s provisioned template ${roomSnap.name}@${roomSnap.version}`));
963
+ throw taskRoomPublicError('template_mismatch', {
964
+ requested: `${snap.name}@${snap.version}`, room: t.room_id,
965
+ provisioned: `${roomSnap.name}@${roomSnap.version}`,
966
+ });
760
967
  }
761
968
  let templateRef = t.template;
762
969
  if (!templateRef || templateRef.name !== snap.name || templateRef.content_hash !== snap.content_hash) {
@@ -815,26 +1022,30 @@ export function registerTaskCommands(parent, cOpt) {
815
1022
  t = getTask(t.task_id);
816
1023
  }
817
1024
  else {
818
- die(new Error(`task ${id} has room ${t.room_id} in a non-resumable state — run 'task recover ${id}'`));
1025
+ throw taskRoomPublicError('task_non_resumable', { task: id, room: t.room_id });
819
1026
  }
820
1027
  }
821
1028
  if (opts.json) {
822
1029
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
823
1030
  return;
824
1031
  }
825
- console.log(`Task ${t.task_id} · ${t.state}`);
826
- if (templateRef)
827
- console.log(`Template: ${templateRef.name}@${templateRef.version}`);
828
- if (t.room_id)
829
- console.log(`Room: ${t.room_id}`);
830
- if (t.member_roles.length) {
831
- console.log('Agents:');
832
- for (const m of t.member_roles)
833
- console.log(` ${m.name} (${m.cowork_role})`);
834
- }
1032
+ console.log(renderMarkdownResult({
1033
+ icon: '🛠️', title: 'Task work ready',
1034
+ fields: [
1035
+ { label: 'ID', value: t.task_id, kind: 'code' },
1036
+ { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
1037
+ ...(templateRef ? [{ label: 'Template', value: `${templateRef.name}@${templateRef.version}`, kind: 'code' }] : []),
1038
+ ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
1039
+ ],
1040
+ sections: t.member_roles.length ? [{
1041
+ heading: 'Agents', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)}`),
1042
+ }] : [],
1043
+ }));
835
1044
  }
836
1045
  catch (e) {
837
- die(e);
1046
+ if (opts.json)
1047
+ die(e);
1048
+ dieTaskRoom(e);
838
1049
  }
839
1050
  });
840
1051
  cOpt(taskCmd.command('finish <id>'))
@@ -850,7 +1061,7 @@ export function registerTaskCommands(parent, cOpt) {
850
1061
  const outcome = summary ? { summary } : undefined;
851
1062
  let t = getTask(id);
852
1063
  if (TASK_TERMINAL_STATES.includes(t.state))
853
- die(new Error(`task ${id} is already in terminal state '${t.state}'`));
1064
+ throw taskRoomPublicError('task_terminal_already', { task: id, state: t.state });
854
1065
  if (t.state === 'active') {
855
1066
  reviewTask(id);
856
1067
  }
@@ -866,13 +1077,19 @@ export function registerTaskCommands(parent, cOpt) {
866
1077
  return;
867
1078
  }
868
1079
  if (settled.timedOut) {
869
- console.log(`Task ${t.task_id} · ${t.state} (terminal intent accepted/pending; run task recover ${id})`);
1080
+ console.log(renderMarkdownFailure({
1081
+ kind: 'pending', subject: `task finish ${id}`,
1082
+ detail: 'The finish request was accepted and is still being settled.',
1083
+ action: `Run ours-fleet task recover ${id}.`,
1084
+ }));
870
1085
  return;
871
1086
  }
872
- console.log(`Task ${t.task_id} · done`);
1087
+ console.log(taskActionMarkdown('Task finished', t, t.outcome ? [{ label: 'Summary', value: t.outcome.summary, multiline: true }] : []));
873
1088
  }
874
1089
  catch (e) {
875
- die(e);
1090
+ if (opts.json)
1091
+ die(e);
1092
+ dieTaskRoom(e);
876
1093
  }
877
1094
  });
878
1095
  }
@@ -896,7 +1113,7 @@ export function registerRoomCommands(parent, cOpt) {
896
1113
  if (opts.template) {
897
1114
  const t = resolveTemplate(opts.template, allTemplates(cfg));
898
1115
  if (!t)
899
- die(new Error(`template not found: ${opts.template}`));
1116
+ throw taskRoomPublicError('template_not_found', { template: opts.template });
900
1117
  templateSnapshot = snapshotTemplate(t);
901
1118
  }
902
1119
  const record = await provisionRoom(cfg, {
@@ -909,12 +1126,15 @@ export function registerRoomCommands(parent, cOpt) {
909
1126
  console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
910
1127
  return;
911
1128
  }
912
- console.log(`Room ${record.room_id} created · ${record.state}`);
913
- if (templateSnapshot)
914
- console.log(`Template: ${templateSnapshot.name}@${templateSnapshot.version}`);
1129
+ console.log(roomActionMarkdown('Room created', record, [
1130
+ { label: 'Name', value: record.room_name },
1131
+ ...(templateSnapshot ? [{ label: 'Template', value: `${templateSnapshot.name}@${templateSnapshot.version}`, kind: 'code' }] : []),
1132
+ ]));
915
1133
  }
916
1134
  catch (e) {
917
- die(e);
1135
+ if (opts.json)
1136
+ die(e);
1137
+ dieTaskRoom(e);
918
1138
  }
919
1139
  });
920
1140
  cOpt(roomCmd.command('list'))
@@ -925,7 +1145,7 @@ export function registerRoomCommands(parent, cOpt) {
925
1145
  try {
926
1146
  const cfg = loadCfg(opts);
927
1147
  if (opts.state && !['active', 'provisioning', 'all'].includes(opts.state))
928
- throw new Error('room state filter must be active, provisioning, or all');
1148
+ throw taskRoomPublicError('room_filter');
929
1149
  const stateFilter = opts.state === 'active' || opts.state === 'provisioning'
930
1150
  ? opts.state : undefined;
931
1151
  const adapter = coworkFor(cfg);
@@ -944,15 +1164,16 @@ export function registerRoomCommands(parent, cOpt) {
944
1164
  console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
945
1165
  return;
946
1166
  }
947
- if (!rooms.length) {
948
- console.log('No rooms.');
949
- return;
950
- }
951
- for (const r of rooms)
952
- console.log(`${r.room_id} ${r.state} ${r.room_name}${r.orchestration?.task_id ? ` (task: ${r.orchestration.task_id})` : ''}`);
1167
+ console.log(renderMarkdownList({
1168
+ icon: '🏠', title: 'Rooms', empty: 'No rooms found.',
1169
+ records: rooms.map(r => `${roomStatus(r.state)} ${markdownCode(r.room_id)} — ${markdownProse(r.room_name)}`
1170
+ + (r.orchestration?.task_id ? ` — Task ${markdownCode(r.orchestration.task_id)}` : '')),
1171
+ }));
953
1172
  }
954
1173
  catch (e) {
955
- die(e);
1174
+ if (opts.json)
1175
+ die(e);
1176
+ dieTaskRoom(e);
956
1177
  }
957
1178
  });
958
1179
  cOpt(roomCmd.command('show <id>'))
@@ -963,39 +1184,40 @@ export function registerRoomCommands(parent, cOpt) {
963
1184
  const cfg = loadCfg(opts);
964
1185
  const tracked = getRoomRecord(id);
965
1186
  if (tracked?.state === 'closing' || tracked?.state === 'closed')
966
- die(new Error(`room not found: ${id}`));
1187
+ throw taskRoomPublicError('room_not_found', { room: id });
967
1188
  const cowork = await coworkFor(cfg).getRoom(id);
968
1189
  if (!cowork || cowork.state === 'closing' || cowork.state === 'closed')
969
- die(new Error(`room not found: ${id}`));
1190
+ throw taskRoomPublicError('room_not_found', { room: id });
970
1191
  const r = tracked;
971
1192
  if (opts.json) {
972
1193
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
973
1194
  return;
974
1195
  }
975
- console.log(`Room: ${cowork.room_id}`);
976
- console.log(`Name: ${cowork.room_name}`);
977
- console.log(`State: ${cowork.state}`);
978
- console.log(`Identity CID: ${cowork.identity_cid}`);
979
- if (r?.task_id)
980
- console.log(`Task: ${r.task_id}`);
981
- if (r)
982
- console.log(`Saga: ${r.saga.phase} (step ${r.saga.step_index})`);
983
- if (r?.provisioning_detail)
984
- console.log(`Detail: ${r.provisioning_detail}`);
985
- if (r?.saga.error)
986
- console.log(`Error: ${r.saga.error}`);
987
- if (cowork.seats.length) {
988
- console.log('Members:');
989
- for (const s of cowork.seats)
990
- console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
991
- }
992
- if (r)
993
- printRoomStartupEvidence(r);
994
- if (r)
995
- console.log(`Tracked by Fleet since: ${r.created_at}`);
1196
+ console.log(renderMarkdownResult({
1197
+ icon: '🏠', title: 'Room details',
1198
+ fields: [
1199
+ { label: 'ID', value: cowork.room_id, kind: 'code' },
1200
+ { label: 'Name', value: cowork.room_name },
1201
+ { label: 'Status', value: roomStatus(cowork.state), kind: 'markdown' },
1202
+ { label: 'Identity CID', value: cowork.identity_cid, kind: 'code' },
1203
+ ...(r?.task_id ? [{ label: 'Task', value: r.task_id, kind: 'code' }] : []),
1204
+ ...(r ? [{ label: 'Saga', value: `${r.saga.phase} (step ${r.saga.step_index})` }] : []),
1205
+ ...(r?.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
1206
+ ...(r?.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
1207
+ ...(r ? [{ label: 'Tracked since', value: r.created_at, kind: 'code' }] : []),
1208
+ ],
1209
+ sections: [
1210
+ ...(cowork.seats.length ? [{
1211
+ heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`),
1212
+ }] : []),
1213
+ ...(r ? roomStartupSections(r) : []),
1214
+ ],
1215
+ }));
996
1216
  }
997
1217
  catch (e) {
998
- die(e);
1218
+ if (opts.json)
1219
+ die(e);
1220
+ dieTaskRoom(e);
999
1221
  }
1000
1222
  });
1001
1223
  cOpt(roomCmd.command('open <id>'))
@@ -1006,20 +1228,28 @@ export function registerRoomCommands(parent, cOpt) {
1006
1228
  const cfg = loadCfg(opts);
1007
1229
  const tracked = getRoomRecord(id);
1008
1230
  if (tracked?.state === 'closing' || tracked?.state === 'closed')
1009
- die(new Error(`room not found: ${id}`));
1231
+ throw taskRoomPublicError('room_not_found', { room: id });
1010
1232
  const room = await coworkFor(cfg).getRoom(id);
1011
1233
  if (!room || room.state === 'closing' || room.state === 'closed')
1012
- die(new Error(`room not found: ${id}`));
1234
+ throw taskRoomPublicError('room_not_found', { room: id });
1013
1235
  const url = `http://localhost:4460/room/${id}`;
1014
1236
  if (opts.json) {
1015
1237
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
1016
1238
  return;
1017
1239
  }
1018
- console.log(`Room ${id} — ${room.room_name}`);
1019
- console.log(`Local console: ${url}`);
1240
+ console.log(renderMarkdownResult({
1241
+ icon: '🏠', title: 'Room console',
1242
+ fields: [
1243
+ { label: 'Room', value: id, kind: 'code' },
1244
+ { label: 'Name', value: room.room_name },
1245
+ { label: 'Local console', value: url, kind: 'code' },
1246
+ ],
1247
+ }));
1020
1248
  }
1021
1249
  catch (e) {
1022
- die(e);
1250
+ if (opts.json)
1251
+ die(e);
1252
+ dieTaskRoom(e);
1023
1253
  }
1024
1254
  });
1025
1255
  cOpt(roomCmd.command('members <id>'))
@@ -1030,11 +1260,11 @@ export function registerRoomCommands(parent, cOpt) {
1030
1260
  const cfg = loadCfg(opts);
1031
1261
  const r = getRoomRecord(id);
1032
1262
  if (r?.state === 'closing' || r?.state === 'closed')
1033
- die(new Error(`room not found: ${id}`));
1263
+ throw taskRoomPublicError('room_not_found', { room: id });
1034
1264
  const adapter = coworkFor(cfg);
1035
1265
  const room = await adapter.getRoom(id);
1036
1266
  if (!room || room.state === 'closing' || room.state === 'closed')
1037
- die(new Error(`room not found: ${id}`));
1267
+ throw taskRoomPublicError('room_not_found', { room: id });
1038
1268
  const members = await adapter.getSeats(id);
1039
1269
  if (opts.json) {
1040
1270
  console.log(JSON.stringify({
@@ -1045,28 +1275,34 @@ export function registerRoomCommands(parent, cOpt) {
1045
1275
  }, null, 2));
1046
1276
  return;
1047
1277
  }
1048
- console.log(`Room ${id}${r ? ` — ${r.room_name}` : ''}`);
1049
- if (r?.owner_seat_cid)
1050
- console.log(`Owner: ${r.owner_seat_cid}`);
1051
- if (!members.length) {
1052
- console.log('No members.');
1053
- return;
1054
- }
1055
- for (const s of members)
1056
- console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
1278
+ console.log(renderMarkdownResult({
1279
+ icon: '👥', title: 'Room members',
1280
+ fields: [
1281
+ { label: 'Room', value: id, kind: 'code' },
1282
+ ...(r ? [{ label: 'Name', value: r.room_name }] : []),
1283
+ ...(r?.owner_seat_cid ? [{ label: 'Owner', value: r.owner_seat_cid, kind: 'code' }] : []),
1284
+ ],
1285
+ sections: [{
1286
+ heading: 'Members',
1287
+ ...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`) }
1288
+ : { items: ['No members found.'] }),
1289
+ }],
1290
+ }));
1057
1291
  }
1058
1292
  catch (e) {
1059
- die(e);
1293
+ if (opts.json)
1294
+ die(e);
1295
+ dieTaskRoom(e);
1060
1296
  }
1061
1297
  });
1062
1298
  const deleteAction = async (id, confirmId, opts) => {
1063
1299
  try {
1064
1300
  if (id !== confirmId)
1065
- die(new Error('confirmation ID must match room ID'));
1301
+ throw taskRoomPublicError('room_confirmation_mismatch');
1066
1302
  coworkFor(loadCfg(opts));
1067
1303
  const existing = getRoomRecord(id);
1068
1304
  if (!existing)
1069
- throw new Error(`room not found in Fleet orchestration: ${id}`);
1305
+ throw taskRoomPublicError('room_record_not_found', { room: id });
1070
1306
  await acceptManagedRoomClose(id);
1071
1307
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1072
1308
  if (opts.json) {
@@ -1074,13 +1310,22 @@ export function registerRoomCommands(parent, cOpt) {
1074
1310
  return;
1075
1311
  }
1076
1312
  if (settled.timedOut) {
1077
- console.log(`Room ${id} · deletion accepted/pending; run room delete ${id} ${id}`);
1313
+ console.log(renderMarkdownFailure({
1314
+ kind: 'pending', subject: `room delete ${id} ${id}`,
1315
+ detail: 'The deletion request was accepted and is still being settled.',
1316
+ action: `Run ours-fleet room delete ${id} ${id} or room recover ${id}.`,
1317
+ }));
1078
1318
  return;
1079
1319
  }
1080
- console.log(`Room ${id} · deleted`);
1320
+ console.log(renderMarkdownResult({
1321
+ icon: '🗑️', title: 'Room deleted',
1322
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1323
+ }));
1081
1324
  }
1082
1325
  catch (e) {
1083
- die(e);
1326
+ if (opts.json)
1327
+ die(e);
1328
+ dieTaskRoom(e);
1084
1329
  }
1085
1330
  };
1086
1331
  cOpt(roomCmd.command('delete <id> <confirm-id>'))
@@ -1110,9 +1355,16 @@ export function registerRoomCommands(parent, cOpt) {
1110
1355
  }, null, 2));
1111
1356
  return;
1112
1357
  }
1113
- console.log(settled.timedOut
1114
- ? `Room ${id} · deletion pending`
1115
- : `Room ${id} · deleted`);
1358
+ console.log(renderMarkdownResult({
1359
+ icon: settled.timedOut ? '⏳' : '🗑️',
1360
+ title: settled.timedOut ? 'Room deletion pending' : 'Room deleted',
1361
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1362
+ sections: [{ heading: 'Next step', items: [
1363
+ settled.timedOut
1364
+ ? `Run ours-fleet room delete ${id} ${id} or room recover ${id} again.`
1365
+ : 'No recovery action is needed.',
1366
+ ] }],
1367
+ }));
1116
1368
  return;
1117
1369
  }
1118
1370
  const cowork = await adapter.recoverRoom(id);
@@ -1138,9 +1390,9 @@ export function registerRoomCommands(parent, cOpt) {
1138
1390
  }
1139
1391
  const actions = [];
1140
1392
  if (r?.saga.error)
1141
- actions.push(`Last error: ${r.saga.error}`);
1393
+ actions.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
1142
1394
  if (r?.saga.recovery_hint)
1143
- actions.push(r.saga.recovery_hint);
1395
+ actions.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
1144
1396
  if (r?.provisioning_detail === 'waiting_cowork')
1145
1397
  actions.push('Check ours-cowork service status');
1146
1398
  if (r?.provisioning_detail === 'waiting_owner_invite')
@@ -1180,18 +1432,22 @@ export function registerRoomCommands(parent, cOpt) {
1180
1432
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
1181
1433
  return;
1182
1434
  }
1183
- console.log(`Room ${cowork.room_id} · ${cowork.state}${r ? ` · saga: ${r.saga.phase}` : ''}`);
1184
- if (actions.length) {
1185
- console.log('Recovery:');
1186
- for (const a of actions)
1187
- console.log(` - ${a}`);
1188
- }
1189
- else {
1190
- console.log('No recovery actions needed.');
1191
- }
1435
+ console.log(renderMarkdownResult({
1436
+ icon: '🛟', title: 'Room recovery',
1437
+ fields: [
1438
+ { label: 'Room', value: cowork.room_id, kind: 'code' },
1439
+ { label: 'Status', value: roomStatus(cowork.state), kind: 'markdown' },
1440
+ ...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : []),
1441
+ ],
1442
+ sections: actions.length
1443
+ ? [{ heading: 'Next steps', items: actions }]
1444
+ : [{ heading: 'Result', items: ['No recovery action is needed.'] }],
1445
+ }));
1192
1446
  }
1193
1447
  catch (e) {
1194
- die(e);
1448
+ if (opts.json)
1449
+ die(e);
1450
+ dieTaskRoom(e);
1195
1451
  }
1196
1452
  });
1197
1453
  const internalDeleteAction = async (id, opts) => {
@@ -1202,11 +1458,16 @@ export function registerRoomCommands(parent, cOpt) {
1202
1458
  console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
1203
1459
  return;
1204
1460
  }
1205
- console.log(`Room ${id} · deleted`);
1461
+ console.log(renderMarkdownResult({
1462
+ icon: '🗑️', title: 'Room deleted',
1463
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1464
+ }));
1206
1465
  }
1207
1466
  catch (e) {
1208
1467
  await recordManagedRoomCloseError(id, errorText(e), `External delete worker failed. Retry room delete ${id} ${id}.`).catch(() => { });
1209
- die(e);
1468
+ if (opts.json)
1469
+ die(e);
1470
+ dieTaskRoom(e);
1210
1471
  }
1211
1472
  };
1212
1473
  cOpt(roomCmd.command('_delete <id>', { hidden: true }))