@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.
- package/README.md +96 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +145 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1,16 +1,227 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { loadConfig, ConfigError } from '../config.js';
|
|
3
|
-
import { provisionMembers,
|
|
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,
|
|
6
|
-
import { createRoomRecord, getRoomRecord, listRoomRecords,
|
|
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
|
|
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
|
|
22
|
-
throw new ConfigError('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
|
-
|
|
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(
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
609
|
+
console.log(taskActionMarkdown('Task started', t));
|
|
356
610
|
}
|
|
357
611
|
catch (e) {
|
|
358
|
-
|
|
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(
|
|
628
|
+
console.log(taskActionMarkdown('Task blocked', t, [{ label: 'Reason', value: opts.reason }]));
|
|
373
629
|
}
|
|
374
630
|
catch (e) {
|
|
375
|
-
|
|
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(
|
|
646
|
+
console.log(taskActionMarkdown('Task unblocked', t));
|
|
389
647
|
}
|
|
390
648
|
catch (e) {
|
|
391
|
-
|
|
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(
|
|
664
|
+
console.log(taskActionMarkdown('Task ready for review', t));
|
|
405
665
|
}
|
|
406
666
|
catch (e) {
|
|
407
|
-
|
|
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
|
|
422
|
-
if (
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
const
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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
|
|
961
|
+
const roomSnap = getRoomRecord(t.room_id)?.template_snapshot;
|
|
585
962
|
if (roomSnap && (roomSnap.name !== snap.name || roomSnap.content_hash !== snap.content_hash))
|
|
586
|
-
|
|
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', '
|
|
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
|
-
|
|
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(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1064
|
+
throw taskRoomPublicError('task_terminal_already', { task: id, state: t.state });
|
|
680
1065
|
if (t.state === 'active') {
|
|
681
1066
|
reviewTask(id);
|
|
682
1067
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
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|
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
788
|
-
const r =
|
|
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(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
1293
|
+
if (opts.json)
|
|
1294
|
+
die(e);
|
|
1295
|
+
dieTaskRoom(e);
|
|
867
1296
|
}
|
|
868
1297
|
});
|
|
869
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
await coworkFor(cfg).closeRoom(id);
|
|
1301
|
+
throw taskRoomPublicError('room_confirmation_mismatch');
|
|
1302
|
+
coworkFor(loadCfg(opts));
|
|
878
1303
|
const existing = getRoomRecord(id);
|
|
879
|
-
|
|
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,
|
|
1309
|
+
console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
|
|
882
1310
|
return;
|
|
883
1311
|
}
|
|
884
|
-
|
|
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
|
-
|
|
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(
|
|
1393
|
+
actions.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
921
1394
|
if (r?.saga.recovery_hint)
|
|
922
|
-
actions.push(
|
|
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', '
|
|
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 =
|
|
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.
|
|
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(
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
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
|
}
|