@ours.network/fleet 0.19.0-nightly.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +96 -11
  2. package/dist/application/fleet-query-service.d.ts +1 -1
  3. package/dist/application/fleet-query-service.js +12 -0
  4. package/dist/application/role-creation-service.js +3 -1
  5. package/dist/application/types.d.ts +11 -0
  6. package/dist/briefing.d.ts +1 -1
  7. package/dist/briefing.js +103 -10
  8. package/dist/build-info.json +5 -5
  9. package/dist/cli.js +41 -11
  10. package/dist/config.d.ts +21 -7
  11. package/dist/config.js +32 -18
  12. package/dist/creation.d.ts +1 -1
  13. package/dist/docs.d.ts +2 -2
  14. package/dist/docs.js +145 -24
  15. package/dist/doctor.js +10 -10
  16. package/dist/duration.js +1 -1
  17. package/dist/fleet-proxy.d.ts +5 -0
  18. package/dist/harness/acp-agent.js +11 -6
  19. package/dist/harness/claude-code.js +201 -8
  20. package/dist/harness/codex.d.ts +4 -1
  21. package/dist/harness/codex.js +72 -13
  22. package/dist/harness/types.d.ts +57 -6
  23. package/dist/isolation/bubblewrap.d.ts +4 -4
  24. package/dist/isolation/bubblewrap.js +4 -4
  25. package/dist/isolation/policy.d.ts +1 -1
  26. package/dist/isolation/policy.js +2 -2
  27. package/dist/isolation/registry.d.ts +3 -3
  28. package/dist/isolation/registry.js +5 -5
  29. package/dist/isolation/resources.d.ts +1 -1
  30. package/dist/isolation/resources.js +1 -1
  31. package/dist/isolation/types.d.ts +3 -3
  32. package/dist/loops/manager.d.ts +30 -1
  33. package/dist/loops/manager.js +69 -6
  34. package/dist/loops/state.d.ts +18 -0
  35. package/dist/loops/state.js +4 -0
  36. package/dist/model-env.d.ts +71 -0
  37. package/dist/model-env.js +106 -0
  38. package/dist/monitor.d.ts +2 -2
  39. package/dist/monitor.js +4 -4
  40. package/dist/ops.d.ts +2 -2
  41. package/dist/ops.js +9 -9
  42. package/dist/owner-channel/channel.d.ts +17 -0
  43. package/dist/owner-channel/channel.js +104 -23
  44. package/dist/owner-channel/commands.d.ts +9 -0
  45. package/dist/owner-channel/commands.js +226 -99
  46. package/dist/owner-channel/notices.d.ts +7 -0
  47. package/dist/owner-channel/notices.js +9 -0
  48. package/dist/rooms-tasks/cli.js +733 -221
  49. package/dist/rooms-tasks/close.d.ts +35 -0
  50. package/dist/rooms-tasks/close.js +182 -0
  51. package/dist/rooms-tasks/config.js +8 -5
  52. package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
  53. package/dist/rooms-tasks/cowork-adapter.js +116 -0
  54. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  55. package/dist/rooms-tasks/external-worker.js +40 -0
  56. package/dist/rooms-tasks/index.d.ts +2 -0
  57. package/dist/rooms-tasks/index.js +2 -0
  58. package/dist/rooms-tasks/markdown.d.ts +49 -0
  59. package/dist/rooms-tasks/markdown.js +206 -0
  60. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  61. package/dist/rooms-tasks/member-startup.js +151 -0
  62. package/dist/rooms-tasks/provision.d.ts +8 -0
  63. package/dist/rooms-tasks/provision.js +363 -97
  64. package/dist/rooms-tasks/room-state.d.ts +13 -1
  65. package/dist/rooms-tasks/room-state.js +149 -1
  66. package/dist/rooms-tasks/task-state.d.ts +13 -2
  67. package/dist/rooms-tasks/task-state.js +131 -3
  68. package/dist/rooms-tasks/terminal.d.ts +23 -0
  69. package/dist/rooms-tasks/terminal.js +56 -0
  70. package/dist/rooms-tasks/types.d.ts +109 -4
  71. package/dist/rooms-tasks/types.js +1 -1
  72. package/dist/runner.d.ts +48 -0
  73. package/dist/runner.js +252 -100
  74. package/dist/session/acp.d.ts +106 -2
  75. package/dist/session/acp.js +217 -14
  76. package/dist/session/activity.d.ts +31 -0
  77. package/dist/session/activity.js +48 -0
  78. package/dist/session/control.d.ts +1 -1
  79. package/dist/session/conversation-normalizer.d.ts +7 -1
  80. package/dist/session/conversation-normalizer.js +154 -11
  81. package/dist/session/conversation-store.js +1 -1
  82. package/dist/session/conversation-types.d.ts +25 -6
  83. package/dist/session/types.d.ts +35 -0
  84. package/dist/spawn.d.ts +4 -2
  85. package/dist/spawn.js +39 -26
  86. package/dist/supervisor/launchd.d.ts +2 -2
  87. package/dist/supervisor/launchd.js +4 -4
  88. package/dist/supervisor/systemd.js +4 -4
  89. package/dist/supervisor/types.d.ts +1 -1
  90. package/dist/temp-lifecycle.d.ts +14 -2
  91. package/dist/temp-lifecycle.js +90 -0
  92. package/dist/watchdog/alerts.d.ts +2 -2
  93. package/dist/watchdog/alerts.js +2 -2
  94. package/dist/watchdog/briefing.d.ts +5 -5
  95. package/dist/watchdog/briefing.js +13 -6
  96. package/dist/watchdog/config.js +1 -1
  97. package/dist/watchdog/query.d.ts +2 -2
  98. package/dist/watchdog/query.js +2 -2
  99. package/dist/watchdog/report.js +2 -2
  100. package/dist/watchdog/run.d.ts +2 -2
  101. package/dist/watchdog/run.js +6 -6
  102. package/dist/watchdog/scheduler.d.ts +7 -7
  103. package/dist/watchdog/scheduler.js +10 -10
  104. package/dist/watchdog/service.d.ts +4 -4
  105. package/dist/watchdog/service.js +5 -6
  106. package/dist/watchdog/store.d.ts +4 -4
  107. package/dist/watchdog/store.js +4 -4
  108. package/dist/web/runtime.js +1 -1
  109. package/dist/web/server.js +1 -1
  110. package/dist/web/topology-promote.js +1 -1
  111. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
  112. package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
  113. package/dist/web-app/index.html +1 -1
  114. package/dist/worklog.d.ts +7 -1
  115. package/dist/worklog.js +191 -39
  116. package/package.json +1 -1
@@ -1,16 +1,227 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { loadConfig, ConfigError } from '../config.js';
3
- import { provisionMembers, cleanupMembers, getBinPath } from './provision.js';
3
+ import { provisionMembers, getBinPath } from './provision.js';
4
+ import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from './close.js';
5
+ import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from './terminal.js';
6
+ import { launchFleetWorker } from './external-worker.js';
4
7
  import { resolveTemplate, listTemplates, snapshotTemplate, hashTemplate, } from './templates.js';
5
- import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, updateTaskRoom, updateTaskTemplate, } from './task-state.js';
6
- import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, } from './room-state.js';
8
+ import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unblockTask, reviewTask, updateTaskRoom, updateTaskTemplate, deleteTask, TaskStateError, } from './task-state.js';
9
+ import { createRoomRecord, getRoomRecord, listRoomRecords, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
7
10
  import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
8
- import { TASK_TERMINAL_STATES } from './types.js';
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
+ }
9
89
  function die(e) {
10
- 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);
11
92
  process.stderr.write(`error: ${msg}\n`);
12
93
  process.exit(1);
13
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
+ });
177
+ const PUBLIC_SETTLE_WAIT_MS = 60_000;
178
+ const PUBLIC_SETTLE_POLL_MS = 100;
179
+ const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
180
+ function errorText(error) {
181
+ return error instanceof Error ? error.message : String(error);
182
+ }
183
+ async function launchTaskSettleWorker(taskId, configPath) {
184
+ const previousErrorAt = getTask(taskId).terminal_intent?.error_at;
185
+ try {
186
+ await launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath);
187
+ }
188
+ catch (error) {
189
+ await recordTaskTerminalIntentError(taskId, errorText(error), `External settle worker failed to start. Retry task recover ${taskId}.`);
190
+ throw error;
191
+ }
192
+ const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
193
+ while (Date.now() < deadline) {
194
+ const task = getTask(taskId);
195
+ if (task.terminal_intent?.status === 'settled')
196
+ return { task, timedOut: false };
197
+ if (task.terminal_intent?.error_at !== previousErrorAt && task.terminal_intent?.error) {
198
+ throw new TaskStateError(task.terminal_intent.error);
199
+ }
200
+ await sleep(PUBLIC_SETTLE_POLL_MS);
201
+ }
202
+ return { task: getTask(taskId), timedOut: true };
203
+ }
204
+ async function launchRoomDeleteWorker(roomId, configPath) {
205
+ const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
206
+ try {
207
+ await launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath);
208
+ }
209
+ catch (error) {
210
+ await recordManagedRoomCloseError(roomId, errorText(error), `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`);
211
+ throw error;
212
+ }
213
+ const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
214
+ while (Date.now() < deadline) {
215
+ const room = getRoomRecord(roomId);
216
+ if (!room)
217
+ return { deleted: true, timedOut: false };
218
+ if (room.close?.error_at !== previousErrorAt && room.close?.error) {
219
+ throw new RoomStateError(room.close.error);
220
+ }
221
+ await sleep(PUBLIC_SETTLE_POLL_MS);
222
+ }
223
+ return { deleted: getRoomRecord(roomId) === undefined, timedOut: true };
224
+ }
14
225
  function loadCfg(opts) {
15
226
  return loadConfig(opts.configuration);
16
227
  }
@@ -18,8 +229,8 @@ function allTemplates(cfg) {
18
229
  return cfg.roomTemplates ?? {};
19
230
  }
20
231
  function coworkFor(cfg) {
21
- if (!cfg.rooms || cfg.rooms.provider !== 'cowork')
22
- throw new ConfigError('rooms: configure provider: cowork before creating or querying rooms');
232
+ if (!cfg.rooms)
233
+ throw new ConfigError('rooms: configuration is required before creating or querying rooms');
23
234
  return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
24
235
  }
25
236
  function resolveRoomTemplate(cfg, name) {
@@ -30,6 +241,43 @@ function resolveRoomTemplate(cfg, name) {
30
241
  throw new Error(`template not found: ${name}`);
31
242
  return snapshotTemplate(template);
32
243
  }
244
+ function durableTaskRoomTemplate(task, room) {
245
+ const snapshot = room.template_snapshot;
246
+ if (!snapshot)
247
+ throw new Error(`room ${room.room_id} has no durable template snapshot`);
248
+ const ref = task.template;
249
+ if (!ref || ref.name !== snapshot.name || ref.version !== snapshot.version
250
+ || ref.content_hash !== snapshot.content_hash) {
251
+ throw new Error(`task ${task.task_id} template reference does not match room ${room.room_id}'s durable snapshot`);
252
+ }
253
+ return snapshot;
254
+ }
255
+ function roomStartupSections(room) {
256
+ const sections = [];
257
+ const definitions = Object.values(room.role_briefings ?? {});
258
+ if (definitions.length) {
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
+ });
263
+ }
264
+ if (room.member_seats.length) {
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
+ });
278
+ }
279
+ return sections;
280
+ }
33
281
  async function provisionRoom(cfg, input) {
34
282
  const rooms = cfg.rooms;
35
283
  if (!rooms)
@@ -200,7 +448,7 @@ export function registerTaskCommands(parent, cOpt) {
200
448
  if (opts.template) {
201
449
  const t = resolveTemplate(opts.template, allTemplates(cfg));
202
450
  if (!t)
203
- die(new Error(`template not found: ${opts.template}`));
451
+ throw taskRoomPublicError('template_not_found', { template: opts.template });
204
452
  const snap = snapshotTemplate(t);
205
453
  templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
206
454
  }
@@ -252,12 +500,15 @@ export function registerTaskCommands(parent, cOpt) {
252
500
  console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
253
501
  return;
254
502
  }
255
- console.log(`Task ${record.task_id} created · ${record.state}`);
256
- if (templateRef)
257
- 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
+ ]));
258
507
  }
259
508
  catch (e) {
260
- die(e);
509
+ if (opts.json)
510
+ die(e);
511
+ dieTaskRoom(e);
261
512
  }
262
513
  });
263
514
  cOpt(taskCmd.command('list'))
@@ -275,17 +526,12 @@ export function registerTaskCommands(parent, cOpt) {
275
526
  console.log(JSON.stringify({ schema_version: 1, tasks }, null, 2));
276
527
  return;
277
528
  }
278
- if (!tasks.length) {
279
- console.log('No tasks.');
280
- return;
281
- }
282
- for (const t of tasks) {
283
- const blocked = t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : '';
284
- console.log(`${t.task_id} ${t.state}${blocked} ${t.title}`);
285
- }
529
+ console.log(taskListMarkdown(tasks));
286
530
  }
287
531
  catch (e) {
288
- die(e);
532
+ if (opts.json)
533
+ die(e);
534
+ dieTaskRoom(e);
289
535
  }
290
536
  });
291
537
  taskCmd.command('show <id>')
@@ -294,35 +540,41 @@ export function registerTaskCommands(parent, cOpt) {
294
540
  .action((id, opts) => {
295
541
  try {
296
542
  const t = getTask(id);
543
+ const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
297
544
  if (opts.json) {
298
- console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
545
+ console.log(JSON.stringify({
546
+ schema_version: 1, task: t, orchestration: room ?? null,
547
+ }, null, 2));
299
548
  return;
300
549
  }
301
- console.log(`Task: ${t.task_id}`);
302
- console.log(`Title: ${t.title}`);
303
- console.log(`State: ${t.state}${t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : ''}`);
304
- if (t.template)
305
- console.log(`Template: ${t.template.name}@${t.template.version}`);
306
- if (t.room_id)
307
- console.log(`Room: ${t.room_id}`);
308
- if (t.room_identity_cid)
309
- console.log(`Room CID: ${t.room_identity_cid}`);
310
- if (t.member_roles.length) {
311
- console.log('Members:');
312
- for (const m of t.member_roles)
313
- console.log(` ${m.name} (${m.cowork_role}) ${m.identity_cid}`);
314
- }
315
- console.log(`Origin: ${t.origin.type}`);
316
- console.log(`Created: ${t.created_at}`);
317
- if (t.started_at)
318
- console.log(`Started: ${t.started_at}`);
319
- if (t.ended_at)
320
- console.log(`Ended: ${t.ended_at}`);
321
- if (t.outcome)
322
- 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
+ }));
323
573
  }
324
574
  catch (e) {
325
- die(e);
575
+ if (opts.json)
576
+ die(e);
577
+ dieTaskRoom(e);
326
578
  }
327
579
  });
328
580
  cOpt(taskCmd.command('start <id>'))
@@ -335,7 +587,9 @@ export function registerTaskCommands(parent, cOpt) {
335
587
  if (t.template && !t.room_id) {
336
588
  const template = resolveRoomTemplate(cfg, t.template.name);
337
589
  if (!template || template.content_hash !== t.template.content_hash)
338
- 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
+ });
339
593
  await provisionRoom(cfg, {
340
594
  name: t.title,
341
595
  goal: t.title,
@@ -352,10 +606,12 @@ export function registerTaskCommands(parent, cOpt) {
352
606
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
353
607
  return;
354
608
  }
355
- console.log(`Task ${t.task_id} · provisioning`);
609
+ console.log(taskActionMarkdown('Task started', t));
356
610
  }
357
611
  catch (e) {
358
- die(e);
612
+ if (opts.json)
613
+ die(e);
614
+ dieTaskRoom(e);
359
615
  }
360
616
  });
361
617
  taskCmd.command('block <id>')
@@ -369,10 +625,12 @@ export function registerTaskCommands(parent, cOpt) {
369
625
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
370
626
  return;
371
627
  }
372
- console.log(`Task ${t.task_id} · blocked: ${opts.reason}`);
628
+ console.log(taskActionMarkdown('Task blocked', t, [{ label: 'Reason', value: opts.reason }]));
373
629
  }
374
630
  catch (e) {
375
- die(e);
631
+ if (opts.json)
632
+ die(e);
633
+ dieTaskRoom(e);
376
634
  }
377
635
  });
378
636
  taskCmd.command('unblock <id>')
@@ -385,10 +643,12 @@ export function registerTaskCommands(parent, cOpt) {
385
643
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
386
644
  return;
387
645
  }
388
- console.log(`Task ${t.task_id} · unblocked`);
646
+ console.log(taskActionMarkdown('Task unblocked', t));
389
647
  }
390
648
  catch (e) {
391
- die(e);
649
+ if (opts.json)
650
+ die(e);
651
+ dieTaskRoom(e);
392
652
  }
393
653
  });
394
654
  taskCmd.command('review <id>')
@@ -401,10 +661,12 @@ export function registerTaskCommands(parent, cOpt) {
401
661
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
402
662
  return;
403
663
  }
404
- console.log(`Task ${t.task_id} · review`);
664
+ console.log(taskActionMarkdown('Task ready for review', t));
405
665
  }
406
666
  catch (e) {
407
- die(e);
667
+ if (opts.json)
668
+ die(e);
669
+ dieTaskRoom(e);
408
670
  }
409
671
  });
410
672
  cOpt(taskCmd.command('done <id>'))
@@ -418,29 +680,44 @@ export function registerTaskCommands(parent, cOpt) {
418
680
  if (opts.summaryFile)
419
681
  summary = readFileSync(opts.summaryFile, 'utf8');
420
682
  const outcome = summary ? { summary } : undefined;
421
- const t = completeTask(id, outcome);
422
- if (t.room_id) {
423
- const cfg = loadCfg(opts);
683
+ const current = getTask(id);
684
+ if (TASK_TERMINAL_STATES.includes(current.state))
685
+ throw new TaskStateError(`task ${id} is already in terminal state '${current.state}'`);
686
+ if (current.state !== 'review')
687
+ throw new TaskStateError(`cannot transition from '${current.state}' to 'done'`);
688
+ let roomId;
689
+ let cfg;
690
+ if (current.room_id) {
691
+ cfg = loadCfg(opts);
424
692
  const shouldClose = cfg.tasks?.close_room_on_done
425
693
  ?? cfg.rooms?.defaults?.close_when_task_done
426
694
  ?? false;
427
- if (shouldClose) {
428
- try {
429
- await coworkFor(cfg).closeRoom(t.room_id);
430
- closeRoomRecord(t.room_id);
431
- }
432
- catch { /* room close is best-effort on task done */ }
433
- await cleanupMembers({ roomId: t.room_id, taskId: id }).catch(() => { });
434
- }
695
+ if (shouldClose)
696
+ roomId = current.room_id;
435
697
  }
698
+ await acceptTaskTerminalIntent({ taskId: id, kind: 'done', roomId, outcome });
699
+ const settled = roomId
700
+ ? await launchTaskSettleWorker(id, opts.configuration)
701
+ : { task: getTask(id), timedOut: false };
702
+ const t = settled.task;
436
703
  if (opts.json) {
437
704
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
438
705
  return;
439
706
  }
440
- console.log(`Task ${t.task_id} · done`);
707
+ if (settled.timedOut) {
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
+ }));
713
+ return;
714
+ }
715
+ console.log(taskActionMarkdown('Task completed', t, t.outcome ? [{ label: 'Summary', value: t.outcome.summary, multiline: true }] : []));
441
716
  }
442
717
  catch (e) {
443
- die(e);
718
+ if (opts.json)
719
+ die(e);
720
+ dieTaskRoom(e);
444
721
  }
445
722
  });
446
723
  cOpt(taskCmd.command('cancel <id> <confirm-id>'))
@@ -449,25 +726,60 @@ export function registerTaskCommands(parent, cOpt) {
449
726
  .action(async (id, confirmId, opts) => {
450
727
  try {
451
728
  if (id !== confirmId)
452
- die(new Error('confirmation ID must match task ID'));
453
- const t = cancelTask(id);
454
- if (t.room_id) {
455
- const cfg = loadCfg(opts);
456
- await cleanupMembers({
457
- roomId: t.room_id,
458
- taskId: id,
459
- closeCoworkRoom: true,
460
- cowork: coworkFor(cfg),
461
- }).catch(() => { });
462
- }
729
+ throw taskRoomPublicError('task_confirmation_mismatch');
730
+ const current = getTask(id);
731
+ if (!TASK_CANCELLABLE_STATES.includes(current.state))
732
+ throw new TaskStateError(`cannot cancel a '${current.state}' task`);
733
+ if (current.room_id)
734
+ loadCfg(opts);
735
+ await acceptTaskTerminalIntent({
736
+ taskId: id, kind: 'cancelled', roomId: current.room_id,
737
+ });
738
+ const settled = current.room_id
739
+ ? await launchTaskSettleWorker(id, opts.configuration)
740
+ : { task: getTask(id), timedOut: false };
741
+ const t = settled.task;
463
742
  if (opts.json) {
464
743
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
465
744
  return;
466
745
  }
467
- console.log(`Task ${t.task_id} · cancelled`);
746
+ if (settled.timedOut) {
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
+ }));
752
+ return;
753
+ }
754
+ console.log(taskActionMarkdown('Task cancelled', t));
468
755
  }
469
756
  catch (e) {
470
- die(e);
757
+ if (opts.json)
758
+ die(e);
759
+ dieTaskRoom(e);
760
+ }
761
+ });
762
+ cOpt(taskCmd.command('delete <id> <confirm-id>'))
763
+ .description('delete a done task from the backlog (requires ID twice for confirmation)')
764
+ .option('--json', 'JSON output')
765
+ .action((id, confirmId, opts) => {
766
+ try {
767
+ if (id !== confirmId)
768
+ throw taskRoomPublicError('task_confirmation_mismatch');
769
+ const deleted = deleteTask(id);
770
+ if (opts.json) {
771
+ console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
772
+ return;
773
+ }
774
+ console.log(renderMarkdownResult({
775
+ icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
776
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
777
+ }));
778
+ }
779
+ catch (e) {
780
+ if (opts.json)
781
+ die(e);
782
+ dieTaskRoom(e);
471
783
  }
472
784
  });
473
785
  cOpt(taskCmd.command('recover <id>'))
@@ -476,13 +788,22 @@ export function registerTaskCommands(parent, cOpt) {
476
788
  .action(async (id, opts) => {
477
789
  try {
478
790
  const cfg = loadCfg(opts);
479
- const t = getTask(id);
480
- const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
791
+ let t = getTask(id);
792
+ let terminalTimedOut = false;
793
+ if (t.terminal_intent?.status === 'pending') {
794
+ const settled = await launchTaskSettleWorker(id, opts.configuration);
795
+ t = settled.task;
796
+ terminalTimedOut = settled.timedOut;
797
+ }
798
+ let room = t.room_id ? getRoomRecord(t.room_id) : undefined;
481
799
  const result = {
482
800
  task: t,
483
801
  room: room ?? null,
484
802
  recovery_actions: [],
485
803
  };
804
+ if (terminalTimedOut) {
805
+ result.recovery_actions.push(`Terminal intent remains pending — retry task recover ${id}`);
806
+ }
486
807
  if (t.state === 'provisioning' && room) {
487
808
  if (room.provisioning_detail === 'waiting_cowork')
488
809
  result.recovery_actions.push('Cowork management socket unreachable — check ours-cowork service');
@@ -492,9 +813,14 @@ export function registerTaskCommands(parent, cOpt) {
492
813
  result.recovery_actions.push('Owner CID mismatch — verify rooms.owner.expected_cid matches Messenger identity');
493
814
  if (room.provisioning_detail === 'member_failed')
494
815
  result.recovery_actions.push(`Member creation failed at saga step ${room.saga.step_index} — inspect and retry`);
495
- const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
816
+ if (room.provisioning_detail === 'waiting_briefing_acks')
817
+ result.recovery_actions.push('Members are not ready — inspect per-seat briefing and ACK evidence, then retry recovery');
818
+ if (room.provisioning_detail === 'briefing_delivery_failed')
819
+ result.recovery_actions.push('Cowork briefing relay failed terminally — replace the seat or use a Cowork-supported redelivery');
820
+ const resumable = ['create_members', 'configure_briefings', 'join_role_groups', 'wait_seats',
821
+ 'launch_work', 'wait_briefing_acks', 'activate'];
496
822
  if (resumable.includes(room.saga.phase)) {
497
- const template = t.template ? resolveRoomTemplate(cfg, t.template.name) : undefined;
823
+ const template = durableTaskRoomTemplate(t, room);
498
824
  if (template) {
499
825
  try {
500
826
  await provisionMembers({
@@ -507,7 +833,11 @@ export function registerTaskCommands(parent, cOpt) {
507
833
  brief: t.brief,
508
834
  goal: t.title,
509
835
  });
510
- result.recovery_actions.push('Provisioning resumed successfully');
836
+ t = getTask(t.task_id);
837
+ room = getRoomRecord(room.room_id);
838
+ result.task = t;
839
+ result.room = room ?? null;
840
+ result.recovery_actions = ['Provisioning resumed successfully'];
511
841
  }
512
842
  catch (error) {
513
843
  result.recovery_actions.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -519,20 +849,46 @@ export function registerTaskCommands(parent, cOpt) {
519
849
  console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
520
850
  return;
521
851
  }
522
- console.log(`Task ${t.task_id} · ${t.state}`);
523
- if (room)
524
- console.log(`Room ${room.room_id} · ${room.state} · saga: ${room.saga.phase}`);
525
- if (result.recovery_actions.length) {
526
- console.log('Recovery actions:');
527
- for (const a of result.recovery_actions)
528
- console.log(` - ${a}`);
529
- }
530
- else {
531
- console.log('No automated recovery actions available.');
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
+ }));
867
+ }
868
+ catch (e) {
869
+ if (opts.json)
870
+ die(e);
871
+ dieTaskRoom(e);
872
+ }
873
+ });
874
+ cOpt(taskCmd.command('_settle <id>', { hidden: true }))
875
+ .description('internal: settle a previously accepted task terminal intent')
876
+ .option('--json', 'JSON output')
877
+ .action(async (id, opts) => {
878
+ try {
879
+ const cfg = loadCfg(opts);
880
+ const t = await settleTaskTerminalIntent({ taskId: id, cowork: coworkFor(cfg) });
881
+ if (opts.json) {
882
+ console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
883
+ return;
532
884
  }
885
+ console.log(taskActionMarkdown('Task terminal action settled', t));
533
886
  }
534
887
  catch (e) {
535
- die(e);
888
+ await recordTaskTerminalIntentError(id, errorText(e), `External settle worker failed. Retry task recover ${id}.`).catch(() => { });
889
+ if (opts.json)
890
+ die(e);
891
+ dieTaskRoom(e);
536
892
  }
537
893
  });
538
894
  cOpt(taskCmd.command('work <id>'))
@@ -544,46 +900,70 @@ export function registerTaskCommands(parent, cOpt) {
544
900
  const cfg = loadCfg(opts);
545
901
  let t = getTask(id);
546
902
  if (TASK_TERMINAL_STATES.includes(t.state))
547
- die(new Error(`task ${id} is in terminal state '${t.state}'`));
903
+ throw taskRoomPublicError('task_terminal', { task: id, state: t.state });
548
904
  if (t.state === 'active' && t.room_id) {
549
905
  // An explicit --template must agree with the room the task already
550
906
  // runs in; a conflicting override is an error, not a silent no-op.
551
907
  if (opts.template) {
552
908
  const override = resolveTemplate(opts.template, allTemplates(cfg));
553
909
  if (!override)
554
- die(new Error(`template not found: ${opts.template}`));
910
+ throw taskRoomPublicError('template_not_found', { template: opts.template });
555
911
  const overrideSnap = snapshotTemplate(override);
556
912
  const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
557
913
  if (roomSnap && (roomSnap.name !== overrideSnap.name || roomSnap.content_hash !== overrideSnap.content_hash))
558
- 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
+ });
559
918
  }
560
919
  if (opts.json) {
561
920
  console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
562
921
  return;
563
922
  }
564
- 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' }] : []));
565
924
  return;
566
925
  }
567
926
  // Select and validate the template snapshot before any state change or
568
927
  // provisioning. An explicit --template re-pins the task; a stored ref
569
928
  // must still match its snapshot (same contract as `task start`).
929
+ const existingRoom = t.room_id ? getRoomRecord(t.room_id) : undefined;
930
+ const durableSnapshot = existingRoom ? durableTaskRoomTemplate(t, existingRoom) : undefined;
570
931
  const templateName = opts.template
932
+ ?? durableSnapshot?.name
571
933
  ?? t.template?.name
572
934
  ?? cfg.tasks?.default_room_template
573
935
  ?? cfg.rooms?.defaults?.template
574
936
  ?? 'single';
575
- const resolved = resolveTemplate(templateName, allTemplates(cfg));
576
- if (!resolved)
577
- die(new Error(`template not found: ${templateName}`));
578
- const snap = snapshotTemplate(resolved);
937
+ const resolved = durableSnapshot && !opts.template
938
+ ? undefined : resolveTemplate(templateName, allTemplates(cfg));
939
+ if (!durableSnapshot && !resolved)
940
+ throw taskRoomPublicError('template_not_found', { template: templateName });
941
+ if (opts.template && !resolved)
942
+ throw taskRoomPublicError('template_not_found', { template: templateName });
943
+ if (durableSnapshot && resolved) {
944
+ const requested = snapshotTemplate(resolved);
945
+ if (requested.name !== durableSnapshot.name
946
+ || requested.content_hash !== durableSnapshot.content_hash) {
947
+ throw taskRoomPublicError('template_mismatch', {
948
+ requested: `${requested.name}@${requested.version}`, room: existingRoom.room_id,
949
+ provisioned: `${durableSnapshot.name}@${durableSnapshot.version}`,
950
+ });
951
+ }
952
+ }
953
+ const snap = durableSnapshot ?? snapshotTemplate(resolved);
579
954
  if (!opts.template && t.template && snap.content_hash !== t.template.content_hash)
580
- 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
+ });
581
958
  // A provisioned room is pinned to its snapshot: never resume or re-pin
582
959
  // an existing room under a different template.
583
960
  if (t.room_id) {
584
- const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
961
+ const roomSnap = getRoomRecord(t.room_id)?.template_snapshot;
585
962
  if (roomSnap && (roomSnap.name !== snap.name || roomSnap.content_hash !== snap.content_hash))
586
- 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
+ });
587
967
  }
588
968
  let templateRef = t.template;
589
969
  if (!templateRef || templateRef.name !== snap.name || templateRef.content_hash !== snap.content_hash) {
@@ -618,7 +998,8 @@ export function registerTaskCommands(parent, cOpt) {
618
998
  // The task already has a room mid-provisioning (e.g. seats were still
619
999
  // pending on the last run). Resume it exactly as `task recover` does.
620
1000
  const room = getRoomRecord(t.room_id);
621
- const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
1001
+ const resumable = ['create_members', 'configure_briefings', 'join_role_groups', 'wait_seats',
1002
+ 'launch_work', 'wait_briefing_acks', 'activate'];
622
1003
  if (room && resumable.includes(room.saga.phase)) {
623
1004
  try {
624
1005
  await provisionMembers({
@@ -641,26 +1022,30 @@ export function registerTaskCommands(parent, cOpt) {
641
1022
  t = getTask(t.task_id);
642
1023
  }
643
1024
  else {
644
- 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 });
645
1026
  }
646
1027
  }
647
1028
  if (opts.json) {
648
1029
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
649
1030
  return;
650
1031
  }
651
- console.log(`Task ${t.task_id} · ${t.state}`);
652
- if (templateRef)
653
- console.log(`Template: ${templateRef.name}@${templateRef.version}`);
654
- if (t.room_id)
655
- console.log(`Room: ${t.room_id}`);
656
- if (t.member_roles.length) {
657
- console.log('Agents:');
658
- for (const m of t.member_roles)
659
- console.log(` ${m.name} (${m.cowork_role})`);
660
- }
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
+ }));
661
1044
  }
662
1045
  catch (e) {
663
- die(e);
1046
+ if (opts.json)
1047
+ die(e);
1048
+ dieTaskRoom(e);
664
1049
  }
665
1050
  });
666
1051
  cOpt(taskCmd.command('finish <id>'))
@@ -676,31 +1061,35 @@ export function registerTaskCommands(parent, cOpt) {
676
1061
  const outcome = summary ? { summary } : undefined;
677
1062
  let t = getTask(id);
678
1063
  if (TASK_TERMINAL_STATES.includes(t.state))
679
- die(new Error(`task ${id} is already in terminal state '${t.state}'`));
1064
+ throw taskRoomPublicError('task_terminal_already', { task: id, state: t.state });
680
1065
  if (t.state === 'active') {
681
1066
  reviewTask(id);
682
1067
  }
683
- t = completeTask(id, outcome);
684
- if (t.room_id) {
685
- const cfg = loadCfg(opts);
686
- try {
687
- await cleanupMembers({
688
- roomId: t.room_id,
689
- taskId: id,
690
- closeCoworkRoom: true,
691
- cowork: coworkFor(cfg),
692
- });
693
- }
694
- catch { /* room cleanup is best-effort */ }
695
- }
1068
+ if (t.room_id)
1069
+ loadCfg(opts); // validate configuration before durable acceptance
1070
+ await acceptTaskTerminalIntent({ taskId: id, kind: 'done', roomId: t.room_id, outcome });
1071
+ const settled = t.room_id
1072
+ ? await launchTaskSettleWorker(id, opts.configuration)
1073
+ : { task: getTask(id), timedOut: false };
1074
+ t = settled.task;
696
1075
  if (opts.json) {
697
1076
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
698
1077
  return;
699
1078
  }
700
- console.log(`Task ${t.task_id} · done`);
1079
+ if (settled.timedOut) {
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
+ }));
1085
+ return;
1086
+ }
1087
+ console.log(taskActionMarkdown('Task finished', t, t.outcome ? [{ label: 'Summary', value: t.outcome.summary, multiline: true }] : []));
701
1088
  }
702
1089
  catch (e) {
703
- die(e);
1090
+ if (opts.json)
1091
+ die(e);
1092
+ dieTaskRoom(e);
704
1093
  }
705
1094
  });
706
1095
  }
@@ -724,7 +1113,7 @@ export function registerRoomCommands(parent, cOpt) {
724
1113
  if (opts.template) {
725
1114
  const t = resolveTemplate(opts.template, allTemplates(cfg));
726
1115
  if (!t)
727
- die(new Error(`template not found: ${opts.template}`));
1116
+ throw taskRoomPublicError('template_not_found', { template: opts.template });
728
1117
  templateSnapshot = snapshotTemplate(t);
729
1118
  }
730
1119
  const record = await provisionRoom(cfg, {
@@ -737,43 +1126,54 @@ export function registerRoomCommands(parent, cOpt) {
737
1126
  console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
738
1127
  return;
739
1128
  }
740
- console.log(`Room ${record.room_id} created · ${record.state}`);
741
- if (templateSnapshot)
742
- 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
+ ]));
743
1133
  }
744
1134
  catch (e) {
745
- die(e);
1135
+ if (opts.json)
1136
+ die(e);
1137
+ dieTaskRoom(e);
746
1138
  }
747
1139
  });
748
1140
  cOpt(roomCmd.command('list'))
749
1141
  .description('list rooms')
750
- .option('--state <state>', 'filter by state (active|provisioning|closing|closed|all)')
1142
+ .option('--state <state>', 'filter live rooms by state (active|provisioning|all)')
751
1143
  .option('--json', 'JSON output')
752
1144
  .action(async (opts) => {
753
1145
  try {
754
1146
  const cfg = loadCfg(opts);
755
- let stateFilter;
756
- if (opts.state && opts.state !== 'all') {
757
- stateFilter = opts.state;
758
- }
1147
+ if (opts.state && !['active', 'provisioning', 'all'].includes(opts.state))
1148
+ throw taskRoomPublicError('room_filter');
1149
+ const stateFilter = opts.state === 'active' || opts.state === 'provisioning'
1150
+ ? opts.state : undefined;
1151
+ const adapter = coworkFor(cfg);
1152
+ await deleteLegacyClosedRooms({ cowork: adapter });
759
1153
  const local = new Map(listRoomRecords().map(room => [room.room_id, room]));
760
- const coworkRooms = await coworkFor(cfg).listRooms();
1154
+ const coworkRooms = await adapter.listRooms();
761
1155
  const rooms = coworkRooms
1156
+ .filter(room => room.state === 'active' || room.state === 'provisioning')
1157
+ .filter(room => {
1158
+ const tracked = local.get(room.room_id);
1159
+ return !tracked || tracked.state === 'active' || tracked.state === 'provisioning';
1160
+ })
762
1161
  .filter(room => !stateFilter || room.state === stateFilter)
763
1162
  .map(room => ({ ...room, orchestration: local.get(room.room_id) ?? null }));
764
1163
  if (opts.json) {
765
1164
  console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
766
1165
  return;
767
1166
  }
768
- if (!rooms.length) {
769
- console.log('No rooms.');
770
- return;
771
- }
772
- for (const r of rooms)
773
- 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
+ }));
774
1172
  }
775
1173
  catch (e) {
776
- die(e);
1174
+ if (opts.json)
1175
+ die(e);
1176
+ dieTaskRoom(e);
777
1177
  }
778
1178
  });
779
1179
  cOpt(roomCmd.command('show <id>'))
@@ -782,36 +1182,42 @@ export function registerRoomCommands(parent, cOpt) {
782
1182
  .action(async (id, opts) => {
783
1183
  try {
784
1184
  const cfg = loadCfg(opts);
1185
+ const tracked = getRoomRecord(id);
1186
+ if (tracked?.state === 'closing' || tracked?.state === 'closed')
1187
+ throw taskRoomPublicError('room_not_found', { room: id });
785
1188
  const cowork = await coworkFor(cfg).getRoom(id);
786
- if (!cowork)
787
- die(new Error(`room not found: ${id}`));
788
- const r = getRoomRecord(id);
1189
+ if (!cowork || cowork.state === 'closing' || cowork.state === 'closed')
1190
+ throw taskRoomPublicError('room_not_found', { room: id });
1191
+ const r = tracked;
789
1192
  if (opts.json) {
790
1193
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
791
1194
  return;
792
1195
  }
793
- console.log(`Room: ${cowork.room_id}`);
794
- console.log(`Name: ${cowork.room_name}`);
795
- console.log(`State: ${cowork.state}`);
796
- console.log(`Identity CID: ${cowork.identity_cid}`);
797
- if (r?.task_id)
798
- console.log(`Task: ${r.task_id}`);
799
- if (r)
800
- console.log(`Saga: ${r.saga.phase} (step ${r.saga.step_index})`);
801
- if (r?.provisioning_detail)
802
- console.log(`Detail: ${r.provisioning_detail}`);
803
- if (r?.saga.error)
804
- console.log(`Error: ${r.saga.error}`);
805
- if (cowork.seats.length) {
806
- console.log('Members:');
807
- for (const s of cowork.seats)
808
- console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
809
- }
810
- if (r)
811
- 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
+ }));
812
1216
  }
813
1217
  catch (e) {
814
- die(e);
1218
+ if (opts.json)
1219
+ die(e);
1220
+ dieTaskRoom(e);
815
1221
  }
816
1222
  });
817
1223
  cOpt(roomCmd.command('open <id>'))
@@ -820,19 +1226,30 @@ export function registerRoomCommands(parent, cOpt) {
820
1226
  .action(async (id, opts) => {
821
1227
  try {
822
1228
  const cfg = loadCfg(opts);
1229
+ const tracked = getRoomRecord(id);
1230
+ if (tracked?.state === 'closing' || tracked?.state === 'closed')
1231
+ throw taskRoomPublicError('room_not_found', { room: id });
823
1232
  const room = await coworkFor(cfg).getRoom(id);
824
- if (!room)
825
- die(new Error(`room not found: ${id}`));
1233
+ if (!room || room.state === 'closing' || room.state === 'closed')
1234
+ throw taskRoomPublicError('room_not_found', { room: id });
826
1235
  const url = `http://localhost:4460/room/${id}`;
827
1236
  if (opts.json) {
828
1237
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
829
1238
  return;
830
1239
  }
831
- console.log(`Room ${id} — ${room.room_name}`);
832
- 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
+ }));
833
1248
  }
834
1249
  catch (e) {
835
- die(e);
1250
+ if (opts.json)
1251
+ die(e);
1252
+ dieTaskRoom(e);
836
1253
  }
837
1254
  });
838
1255
  cOpt(roomCmd.command('members <id>'))
@@ -842,7 +1259,13 @@ export function registerRoomCommands(parent, cOpt) {
842
1259
  try {
843
1260
  const cfg = loadCfg(opts);
844
1261
  const r = getRoomRecord(id);
845
- const members = await coworkFor(cfg).getSeats(id);
1262
+ if (r?.state === 'closing' || r?.state === 'closed')
1263
+ throw taskRoomPublicError('room_not_found', { room: id });
1264
+ const adapter = coworkFor(cfg);
1265
+ const room = await adapter.getRoom(id);
1266
+ if (!room || room.state === 'closing' || room.state === 'closed')
1267
+ throw taskRoomPublicError('room_not_found', { room: id });
1268
+ const members = await adapter.getSeats(id);
846
1269
  if (opts.json) {
847
1270
  console.log(JSON.stringify({
848
1271
  schema_version: 1,
@@ -852,41 +1275,67 @@ export function registerRoomCommands(parent, cOpt) {
852
1275
  }, null, 2));
853
1276
  return;
854
1277
  }
855
- console.log(`Room ${id}${r ? ` — ${r.room_name}` : ''}`);
856
- if (r?.owner_seat_cid)
857
- console.log(`Owner: ${r.owner_seat_cid}`);
858
- if (!members.length) {
859
- console.log('No members.');
860
- return;
861
- }
862
- for (const s of members)
863
- 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
+ }));
864
1291
  }
865
1292
  catch (e) {
866
- die(e);
1293
+ if (opts.json)
1294
+ die(e);
1295
+ dieTaskRoom(e);
867
1296
  }
868
1297
  });
869
- cOpt(roomCmd.command('close <id> <confirm-id>'))
870
- .description('close a room (requires ID twice for confirmation)')
871
- .option('--json', 'JSON output')
872
- .action(async (id, confirmId, opts) => {
1298
+ const deleteAction = async (id, confirmId, opts) => {
873
1299
  try {
874
1300
  if (id !== confirmId)
875
- die(new Error('confirmation ID must match room ID'));
876
- const cfg = loadCfg(opts);
877
- await coworkFor(cfg).closeRoom(id);
1301
+ throw taskRoomPublicError('room_confirmation_mismatch');
1302
+ coworkFor(loadCfg(opts));
878
1303
  const existing = getRoomRecord(id);
879
- const r = existing ? closeRoomRecord(id) : undefined;
1304
+ if (!existing)
1305
+ throw taskRoomPublicError('room_record_not_found', { room: id });
1306
+ await acceptManagedRoomClose(id);
1307
+ const settled = await launchRoomDeleteWorker(id, opts.configuration);
880
1308
  if (opts.json) {
881
- console.log(JSON.stringify({ schema_version: 1, room_id: id, state: 'closed', orchestration: r ?? null }, null, 2));
1309
+ console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
882
1310
  return;
883
1311
  }
884
- console.log(`Room ${id} · closed`);
1312
+ if (settled.timedOut) {
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
+ }));
1318
+ return;
1319
+ }
1320
+ console.log(renderMarkdownResult({
1321
+ icon: '🗑️', title: 'Room deleted',
1322
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1323
+ }));
885
1324
  }
886
1325
  catch (e) {
887
- die(e);
1326
+ if (opts.json)
1327
+ die(e);
1328
+ dieTaskRoom(e);
888
1329
  }
889
- });
1330
+ };
1331
+ cOpt(roomCmd.command('delete <id> <confirm-id>'))
1332
+ .description('delete a room (requires ID twice for confirmation)')
1333
+ .option('--json', 'JSON output')
1334
+ .action(deleteAction);
1335
+ cOpt(roomCmd.command('close <id> <confirm-id>'))
1336
+ .description('deprecated alias for room delete (requires ID twice for confirmation)')
1337
+ .option('--json', 'JSON output')
1338
+ .action(deleteAction);
890
1339
  cOpt(roomCmd.command('recover <id>'))
891
1340
  .description('attempt to recover a stuck room')
892
1341
  .option('--json', 'JSON output')
@@ -894,8 +1343,32 @@ export function registerRoomCommands(parent, cOpt) {
894
1343
  try {
895
1344
  const cfg = loadCfg(opts);
896
1345
  const adapter = coworkFor(cfg);
897
- const cowork = await adapter.recoverRoom(id);
898
1346
  let r = getRoomRecord(id);
1347
+ if (r?.state === 'closing' || r?.state === 'closed') {
1348
+ const settled = await launchRoomDeleteWorker(id, opts.configuration);
1349
+ if (opts.json) {
1350
+ console.log(JSON.stringify({
1351
+ schema_version: 1, room: null, orchestration: null,
1352
+ recovery_actions: [settled.timedOut
1353
+ ? `Deletion remains pending — retry room delete ${id} ${id}`
1354
+ : 'Deletion completed successfully'],
1355
+ }, null, 2));
1356
+ return;
1357
+ }
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
+ }));
1368
+ return;
1369
+ }
1370
+ const cowork = await adapter.recoverRoom(id);
1371
+ r = getRoomRecord(id);
899
1372
  if (r && !r.owner_seat_cid
900
1373
  && (r.provisioning_detail === 'waiting_owner_invite'
901
1374
  || r.provisioning_detail === 'owner_cid_mismatch')) {
@@ -917,18 +1390,25 @@ export function registerRoomCommands(parent, cOpt) {
917
1390
  }
918
1391
  const actions = [];
919
1392
  if (r?.saga.error)
920
- actions.push(`Last error: ${r.saga.error}`);
1393
+ actions.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
921
1394
  if (r?.saga.recovery_hint)
922
- actions.push(r.saga.recovery_hint);
1395
+ actions.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
923
1396
  if (r?.provisioning_detail === 'waiting_cowork')
924
1397
  actions.push('Check ours-cowork service status');
925
1398
  if (r?.provisioning_detail === 'waiting_owner_invite')
926
1399
  actions.push('Rotate rooms.owner.public_invite in config, then re-run recover');
1400
+ if (r?.provisioning_detail === 'waiting_briefing_acks')
1401
+ actions.push('Inspect per-seat briefing and ACK evidence, then re-run recover');
1402
+ if (r?.provisioning_detail === 'briefing_delivery_failed')
1403
+ actions.push('Replace the failed seat or use a Cowork-supported briefing redelivery');
927
1404
  if (r && r.state === 'provisioning') {
928
- const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
1405
+ const resumable = ['create_members', 'configure_briefings', 'join_role_groups', 'wait_seats',
1406
+ 'launch_work', 'wait_briefing_acks', 'activate'];
929
1407
  if (resumable.includes(r.saga.phase) && r.template_snapshot) {
930
1408
  try {
931
- const template = resolveRoomTemplate(cfg, r.template_snapshot.name);
1409
+ const template = r.task_id
1410
+ ? durableTaskRoomTemplate(getTask(r.task_id), r)
1411
+ : r.template_snapshot;
932
1412
  if (template) {
933
1413
  await provisionMembers({
934
1414
  cfg,
@@ -940,7 +1420,7 @@ export function registerRoomCommands(parent, cOpt) {
940
1420
  goal: r.room_name,
941
1421
  });
942
1422
  r = getRoomRecord(id);
943
- actions.push('Provisioning resumed successfully');
1423
+ actions.splice(0, actions.length, 'Provisioning resumed successfully');
944
1424
  }
945
1425
  }
946
1426
  catch (error) {
@@ -952,18 +1432,50 @@ export function registerRoomCommands(parent, cOpt) {
952
1432
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
953
1433
  return;
954
1434
  }
955
- console.log(`Room ${cowork.room_id} · ${cowork.state}${r ? ` · saga: ${r.saga.phase}` : ''}`);
956
- if (actions.length) {
957
- console.log('Recovery:');
958
- for (const a of actions)
959
- console.log(` - ${a}`);
960
- }
961
- else {
962
- console.log('No recovery actions needed.');
963
- }
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
+ }));
964
1446
  }
965
1447
  catch (e) {
966
- die(e);
1448
+ if (opts.json)
1449
+ die(e);
1450
+ dieTaskRoom(e);
967
1451
  }
968
1452
  });
1453
+ const internalDeleteAction = async (id, opts) => {
1454
+ try {
1455
+ const cfg = loadCfg(opts);
1456
+ const result = await deleteManagedRoom({ roomId: id, cowork: coworkFor(cfg) });
1457
+ if (opts.json) {
1458
+ console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
1459
+ return;
1460
+ }
1461
+ console.log(renderMarkdownResult({
1462
+ icon: '🗑️', title: 'Room deleted',
1463
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1464
+ }));
1465
+ }
1466
+ catch (e) {
1467
+ await recordManagedRoomCloseError(id, errorText(e), `External delete worker failed. Retry room delete ${id} ${id}.`).catch(() => { });
1468
+ if (opts.json)
1469
+ die(e);
1470
+ dieTaskRoom(e);
1471
+ }
1472
+ };
1473
+ cOpt(roomCmd.command('_delete <id>', { hidden: true }))
1474
+ .description('internal: settle an accepted room deletion')
1475
+ .option('--json', 'JSON output')
1476
+ .action(internalDeleteAction);
1477
+ cOpt(roomCmd.command('_close <id>', { hidden: true }))
1478
+ .description('deprecated internal alias for room _delete')
1479
+ .option('--json', 'JSON output')
1480
+ .action(internalDeleteAction);
969
1481
  }