@ours.network/fleet 0.19.0-nightly.1 → 0.19.0-nightly.3
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/dist/build-info.json +4 -4
- package/dist/cli.js +5 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +44 -3
- package/dist/owner-channel/commands.js +89 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +690 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +171 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
- package/dist/rooms-tasks/cowork-adapter.js +272 -0
- package/dist/rooms-tasks/index.d.ts +7 -0
- package/dist/rooms-tasks/index.js +7 -0
- package/dist/rooms-tasks/room-state.d.ts +23 -0
- package/dist/rooms-tasks/room-state.js +118 -0
- package/dist/rooms-tasks/task-state.d.ts +31 -0
- package/dist/rooms-tasks/task-state.js +173 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +80 -0
- package/dist/rooms-tasks/types.d.ts +153 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { loadConfig, ConfigError } from '../config.js';
|
|
3
|
+
import { resolveTemplate, listTemplates, snapshotTemplate, hashTemplate, } from './templates.js';
|
|
4
|
+
import { createTask, getTask, listTasks, startTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, updateTaskRoom, } from './task-state.js';
|
|
5
|
+
import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, advanceSaga, setOwnerSeat, setSagaError, } from './room-state.js';
|
|
6
|
+
import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
|
|
7
|
+
function die(e) {
|
|
8
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
9
|
+
process.stderr.write(`error: ${msg}\n`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
function loadCfg(opts) {
|
|
13
|
+
return loadConfig(opts.configuration);
|
|
14
|
+
}
|
|
15
|
+
function allTemplates(cfg) {
|
|
16
|
+
return cfg.roomTemplates ?? {};
|
|
17
|
+
}
|
|
18
|
+
function coworkFor(cfg) {
|
|
19
|
+
if (!cfg.rooms || cfg.rooms.provider !== 'cowork')
|
|
20
|
+
throw new ConfigError('rooms: configure provider: cowork before creating or querying rooms');
|
|
21
|
+
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
22
|
+
}
|
|
23
|
+
function resolveRoomTemplate(cfg, name) {
|
|
24
|
+
if (!name)
|
|
25
|
+
return undefined;
|
|
26
|
+
const template = resolveTemplate(name, allTemplates(cfg));
|
|
27
|
+
if (!template)
|
|
28
|
+
throw new Error(`template not found: ${name}`);
|
|
29
|
+
return snapshotTemplate(template);
|
|
30
|
+
}
|
|
31
|
+
async function provisionRoom(cfg, input) {
|
|
32
|
+
const rooms = cfg.rooms;
|
|
33
|
+
if (!rooms)
|
|
34
|
+
throw new ConfigError('rooms: configuration is required');
|
|
35
|
+
const attachOwner = rooms.defaults?.attach_owner !== false;
|
|
36
|
+
if (attachOwner && !cfg.ownerInvite)
|
|
37
|
+
throw new ConfigError('rooms.owner: public_invite or public_invite_file is required when attach_owner is enabled');
|
|
38
|
+
const cowork = coworkFor(cfg);
|
|
39
|
+
const goal = input.goal?.trim() || input.name;
|
|
40
|
+
const briefing = input.brief?.trim() || input.template?.contract?.trim() || goal;
|
|
41
|
+
const created = await cowork.createRoom({
|
|
42
|
+
room_name: input.name,
|
|
43
|
+
goal,
|
|
44
|
+
briefing,
|
|
45
|
+
quiet_membership: input.template?.room?.quiet_membership,
|
|
46
|
+
anonymous: input.template?.room?.anonymous,
|
|
47
|
+
});
|
|
48
|
+
let record = createRoomRecord({
|
|
49
|
+
room_id: created.room_id,
|
|
50
|
+
room_name: input.name,
|
|
51
|
+
room_identity_cid: created.identity_cid,
|
|
52
|
+
task_id: input.taskId,
|
|
53
|
+
template_snapshot: input.template,
|
|
54
|
+
});
|
|
55
|
+
input.onCreated?.(record);
|
|
56
|
+
record = advanceSaga(record.room_id, 'create_room', 1);
|
|
57
|
+
if (attachOwner) {
|
|
58
|
+
try {
|
|
59
|
+
record = advanceSaga(record.room_id, 'attach_owner', 2);
|
|
60
|
+
const accepted = await cowork.acceptInvite(record.room_id, cfg.ownerInvite, {
|
|
61
|
+
role: rooms.owner.role,
|
|
62
|
+
expected_cid: rooms.owner.expected_cid,
|
|
63
|
+
});
|
|
64
|
+
record = setOwnerSeat(record.room_id, accepted.seat_cid, cfg.ownerInviteFingerprint ?? '');
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
68
|
+
setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
|
|
69
|
+
? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
70
|
+
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return advanceSaga(record.room_id, 'create_members', 3);
|
|
75
|
+
}
|
|
76
|
+
export function registerTemplateCommands(parent, cOpt) {
|
|
77
|
+
const templateCmd = parent.command('template').description('room template operations');
|
|
78
|
+
cOpt(templateCmd.command('list'))
|
|
79
|
+
.description('list available room templates')
|
|
80
|
+
.option('--json', 'JSON output')
|
|
81
|
+
.action((opts) => {
|
|
82
|
+
try {
|
|
83
|
+
const cfg = loadCfg(opts);
|
|
84
|
+
const templates = listTemplates(allTemplates(cfg));
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
console.log(JSON.stringify({ schema_version: 1, templates }, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!templates.length) {
|
|
90
|
+
console.log('No templates configured.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const t of templates) {
|
|
94
|
+
const tag = t.builtin ? ' (built-in)' : '';
|
|
95
|
+
console.log(`${t.name}@${t.version}${tag} ${t.description}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
die(e);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
cOpt(templateCmd.command('show <name>'))
|
|
103
|
+
.description('show template details')
|
|
104
|
+
.option('--json', 'JSON output')
|
|
105
|
+
.action((name, opts) => {
|
|
106
|
+
try {
|
|
107
|
+
const cfg = loadCfg(opts);
|
|
108
|
+
const t = resolveTemplate(name, allTemplates(cfg));
|
|
109
|
+
if (!t)
|
|
110
|
+
die(new Error(`template not found: ${name}`));
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
console.log(JSON.stringify({ schema_version: 1, template: { ...t, content_hash: hashTemplate(t) } }, null, 2));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(`${t.name}@${t.version} ${t.description}`);
|
|
116
|
+
if (t.contract)
|
|
117
|
+
console.log(`\nContract:\n${t.contract}`);
|
|
118
|
+
console.log(`\nMembers:`);
|
|
119
|
+
for (const m of t.members)
|
|
120
|
+
console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
|
|
121
|
+
console.log(`\nContent hash: ${hashTemplate(t)}`);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
die(e);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
cOpt(templateCmd.command('validate'))
|
|
128
|
+
.description('validate all room templates')
|
|
129
|
+
.option('--json', 'JSON output')
|
|
130
|
+
.action((opts) => {
|
|
131
|
+
try {
|
|
132
|
+
const cfg = loadCfg(opts);
|
|
133
|
+
const templates = listTemplates(allTemplates(cfg));
|
|
134
|
+
const problems = [];
|
|
135
|
+
for (const t of templates) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
for (const m of t.members) {
|
|
138
|
+
if (!cfg.roles.some(r => r.name === m.role_ref))
|
|
139
|
+
issues.push(`member ${m.slot}: role_ref '${m.role_ref}' not found in fleet roles`);
|
|
140
|
+
}
|
|
141
|
+
if (issues.length)
|
|
142
|
+
problems.push({ template: `${t.name}@${t.version}`, issues });
|
|
143
|
+
}
|
|
144
|
+
if (opts.json) {
|
|
145
|
+
console.log(JSON.stringify({ schema_version: 1, valid: !problems.length, problems }, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!problems.length) {
|
|
149
|
+
console.log('All templates valid.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const p of problems) {
|
|
153
|
+
console.log(`${p.template}:`);
|
|
154
|
+
for (const i of p.issues)
|
|
155
|
+
console.log(` - ${i}`);
|
|
156
|
+
}
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
die(e);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function registerTaskCommands(parent, cOpt) {
|
|
165
|
+
const taskCmd = parent.command('task').description('task lifecycle operations');
|
|
166
|
+
cOpt(taskCmd.command('create'))
|
|
167
|
+
.description('create a new task')
|
|
168
|
+
.requiredOption('--title <title>', 'task title')
|
|
169
|
+
.option('--template <name>', 'room template')
|
|
170
|
+
.option('--brief <text>', 'task brief')
|
|
171
|
+
.option('--brief-file <path>', 'task brief from file')
|
|
172
|
+
.option('--backlog', 'create in backlog (do not start immediately)')
|
|
173
|
+
.option('--no-room', 'create task without a room')
|
|
174
|
+
.option('--idempotency-key <key>', 'idempotency key')
|
|
175
|
+
.option('--json', 'JSON output')
|
|
176
|
+
.action(async (opts) => {
|
|
177
|
+
try {
|
|
178
|
+
const cfg = loadCfg(opts);
|
|
179
|
+
let templateRef;
|
|
180
|
+
if (opts.template) {
|
|
181
|
+
const t = resolveTemplate(opts.template, allTemplates(cfg));
|
|
182
|
+
if (!t)
|
|
183
|
+
die(new Error(`template not found: ${opts.template}`));
|
|
184
|
+
const snap = snapshotTemplate(t);
|
|
185
|
+
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
186
|
+
}
|
|
187
|
+
else if (opts.room !== false) {
|
|
188
|
+
const defaultTpl = cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'development-team';
|
|
189
|
+
const t = resolveTemplate(defaultTpl, allTemplates(cfg));
|
|
190
|
+
if (t) {
|
|
191
|
+
const snap = snapshotTemplate(t);
|
|
192
|
+
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let brief = opts.brief;
|
|
196
|
+
if (opts.briefFile) {
|
|
197
|
+
brief = readFileSync(opts.briefFile, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
const origin = { type: 'cli' };
|
|
200
|
+
let record = createTask({
|
|
201
|
+
title: opts.title,
|
|
202
|
+
brief,
|
|
203
|
+
brief_file: opts.briefFile,
|
|
204
|
+
template: templateRef,
|
|
205
|
+
origin,
|
|
206
|
+
idempotency_key: opts.idempotencyKey,
|
|
207
|
+
start: !opts.backlog,
|
|
208
|
+
});
|
|
209
|
+
if (!opts.backlog && opts.room !== false && templateRef && !record.room_id) {
|
|
210
|
+
const template = resolveRoomTemplate(cfg, templateRef.name);
|
|
211
|
+
try {
|
|
212
|
+
await provisionRoom(cfg, {
|
|
213
|
+
name: opts.title,
|
|
214
|
+
goal: opts.title,
|
|
215
|
+
brief,
|
|
216
|
+
template,
|
|
217
|
+
taskId: record.task_id,
|
|
218
|
+
onCreated: room => {
|
|
219
|
+
record = updateTaskRoom(record.task_id, room.room_id, room.room_identity_cid);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
record = getTask(record.task_id);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof CoworkUnavailableError) {
|
|
226
|
+
blockTask(record.task_id, 'Cowork management socket is unavailable');
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (opts.json) {
|
|
232
|
+
console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.log(`Task ${record.task_id} created · ${record.state}`);
|
|
236
|
+
if (templateRef)
|
|
237
|
+
console.log(`Template: ${templateRef.name}@${templateRef.version}`);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
die(e);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
cOpt(taskCmd.command('list'))
|
|
244
|
+
.description('list tasks')
|
|
245
|
+
.option('--state <state>', 'filter by state (backlog|active|provisioning|review|done|cancelled|failed|all)')
|
|
246
|
+
.option('--json', 'JSON output')
|
|
247
|
+
.action((opts) => {
|
|
248
|
+
try {
|
|
249
|
+
let stateFilter;
|
|
250
|
+
if (opts.state && opts.state !== 'all') {
|
|
251
|
+
stateFilter = opts.state;
|
|
252
|
+
}
|
|
253
|
+
const tasks = listTasks(stateFilter ? { state: stateFilter } : undefined);
|
|
254
|
+
if (opts.json) {
|
|
255
|
+
console.log(JSON.stringify({ schema_version: 1, tasks }, null, 2));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!tasks.length) {
|
|
259
|
+
console.log('No tasks.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
for (const t of tasks) {
|
|
263
|
+
const blocked = t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : '';
|
|
264
|
+
console.log(`${t.task_id} ${t.state}${blocked} ${t.title}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
die(e);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
taskCmd.command('show <id>')
|
|
272
|
+
.description('show task details')
|
|
273
|
+
.option('--json', 'JSON output')
|
|
274
|
+
.action((id, opts) => {
|
|
275
|
+
try {
|
|
276
|
+
const t = getTask(id);
|
|
277
|
+
if (opts.json) {
|
|
278
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
console.log(`Task: ${t.task_id}`);
|
|
282
|
+
console.log(`Title: ${t.title}`);
|
|
283
|
+
console.log(`State: ${t.state}${t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : ''}`);
|
|
284
|
+
if (t.template)
|
|
285
|
+
console.log(`Template: ${t.template.name}@${t.template.version}`);
|
|
286
|
+
if (t.room_id)
|
|
287
|
+
console.log(`Room: ${t.room_id}`);
|
|
288
|
+
if (t.room_identity_cid)
|
|
289
|
+
console.log(`Room CID: ${t.room_identity_cid}`);
|
|
290
|
+
if (t.member_roles.length) {
|
|
291
|
+
console.log('Members:');
|
|
292
|
+
for (const m of t.member_roles)
|
|
293
|
+
console.log(` ${m.name} (${m.cowork_role}) ${m.identity_cid}`);
|
|
294
|
+
}
|
|
295
|
+
console.log(`Origin: ${t.origin.type}`);
|
|
296
|
+
console.log(`Created: ${t.created_at}`);
|
|
297
|
+
if (t.started_at)
|
|
298
|
+
console.log(`Started: ${t.started_at}`);
|
|
299
|
+
if (t.ended_at)
|
|
300
|
+
console.log(`Ended: ${t.ended_at}`);
|
|
301
|
+
if (t.outcome)
|
|
302
|
+
console.log(`Outcome: ${t.outcome.summary}`);
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
die(e);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
cOpt(taskCmd.command('start <id>'))
|
|
309
|
+
.description('start a backlog task')
|
|
310
|
+
.option('--json', 'JSON output')
|
|
311
|
+
.action(async (id, opts) => {
|
|
312
|
+
try {
|
|
313
|
+
const cfg = loadCfg(opts);
|
|
314
|
+
let t = startTask(id);
|
|
315
|
+
if (t.template && !t.room_id) {
|
|
316
|
+
const template = resolveRoomTemplate(cfg, t.template.name);
|
|
317
|
+
if (!template || template.content_hash !== t.template.content_hash)
|
|
318
|
+
throw new Error(`task template snapshot no longer matches ${t.template.name}@${t.template.version}`);
|
|
319
|
+
await provisionRoom(cfg, {
|
|
320
|
+
name: t.title,
|
|
321
|
+
goal: t.title,
|
|
322
|
+
brief: t.brief,
|
|
323
|
+
template,
|
|
324
|
+
taskId: t.task_id,
|
|
325
|
+
onCreated: room => {
|
|
326
|
+
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
t = getTask(t.task_id);
|
|
330
|
+
}
|
|
331
|
+
if (opts.json) {
|
|
332
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.log(`Task ${t.task_id} · provisioning`);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
die(e);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
taskCmd.command('block <id>')
|
|
342
|
+
.description('mark a task as blocked')
|
|
343
|
+
.requiredOption('--reason <reason>', 'blocking reason')
|
|
344
|
+
.option('--json', 'JSON output')
|
|
345
|
+
.action((id, opts) => {
|
|
346
|
+
try {
|
|
347
|
+
const t = blockTask(id, opts.reason);
|
|
348
|
+
if (opts.json) {
|
|
349
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
console.log(`Task ${t.task_id} · blocked: ${opts.reason}`);
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
die(e);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
taskCmd.command('unblock <id>')
|
|
359
|
+
.description('unblock a task')
|
|
360
|
+
.option('--json', 'JSON output')
|
|
361
|
+
.action((id, opts) => {
|
|
362
|
+
try {
|
|
363
|
+
const t = unblockTask(id);
|
|
364
|
+
if (opts.json) {
|
|
365
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.log(`Task ${t.task_id} · unblocked`);
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
die(e);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
taskCmd.command('review <id>')
|
|
375
|
+
.description('move a task to review')
|
|
376
|
+
.option('--json', 'JSON output')
|
|
377
|
+
.action((id, opts) => {
|
|
378
|
+
try {
|
|
379
|
+
const t = reviewTask(id);
|
|
380
|
+
if (opts.json) {
|
|
381
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
console.log(`Task ${t.task_id} · review`);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
die(e);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
taskCmd.command('done <id>')
|
|
391
|
+
.description('complete a task')
|
|
392
|
+
.option('--summary <text>', 'completion summary')
|
|
393
|
+
.option('--summary-file <path>', 'completion summary from file')
|
|
394
|
+
.option('--json', 'JSON output')
|
|
395
|
+
.action((id, opts) => {
|
|
396
|
+
try {
|
|
397
|
+
let summary = opts.summary;
|
|
398
|
+
if (opts.summaryFile)
|
|
399
|
+
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
400
|
+
const outcome = summary ? { summary } : undefined;
|
|
401
|
+
const t = completeTask(id, outcome);
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
console.log(`Task ${t.task_id} · done`);
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
die(e);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
taskCmd.command('cancel <id> <confirm-id>')
|
|
413
|
+
.description('cancel a task (requires ID twice for confirmation)')
|
|
414
|
+
.option('--json', 'JSON output')
|
|
415
|
+
.action((id, confirmId, opts) => {
|
|
416
|
+
try {
|
|
417
|
+
if (id !== confirmId)
|
|
418
|
+
die(new Error('confirmation ID must match task ID'));
|
|
419
|
+
const t = cancelTask(id);
|
|
420
|
+
if (opts.json) {
|
|
421
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
console.log(`Task ${t.task_id} · cancelled`);
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
die(e);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
taskCmd.command('recover <id>')
|
|
431
|
+
.description('attempt to recover a stuck task')
|
|
432
|
+
.option('--json', 'JSON output')
|
|
433
|
+
.action((id, opts) => {
|
|
434
|
+
try {
|
|
435
|
+
const t = getTask(id);
|
|
436
|
+
const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
437
|
+
const result = {
|
|
438
|
+
task: t,
|
|
439
|
+
room: room ?? null,
|
|
440
|
+
recovery_actions: [],
|
|
441
|
+
};
|
|
442
|
+
if (t.state === 'provisioning' && room) {
|
|
443
|
+
if (room.provisioning_detail === 'waiting_cowork')
|
|
444
|
+
result.recovery_actions.push('Cowork management socket unreachable — check ours-cowork service');
|
|
445
|
+
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
446
|
+
result.recovery_actions.push('Owner invite invalid or expired — rotate rooms.owner.public_invite in config');
|
|
447
|
+
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
448
|
+
result.recovery_actions.push('Owner CID mismatch — verify rooms.owner.expected_cid matches Messenger identity');
|
|
449
|
+
if (room.provisioning_detail === 'member_failed')
|
|
450
|
+
result.recovery_actions.push(`Member creation failed at saga step ${room.saga.step_index} — inspect and retry`);
|
|
451
|
+
}
|
|
452
|
+
if (opts.json) {
|
|
453
|
+
console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
console.log(`Task ${t.task_id} · ${t.state}`);
|
|
457
|
+
if (room)
|
|
458
|
+
console.log(`Room ${room.room_id} · ${room.state} · saga: ${room.saga.phase}`);
|
|
459
|
+
if (result.recovery_actions.length) {
|
|
460
|
+
console.log('Recovery actions:');
|
|
461
|
+
for (const a of result.recovery_actions)
|
|
462
|
+
console.log(` - ${a}`);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
console.log('No automated recovery actions available.');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
die(e);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
export function registerRoomCommands(parent, cOpt) {
|
|
474
|
+
const roomCmd = parent.command('room').description('room orchestration operations');
|
|
475
|
+
cOpt(roomCmd.command('create'))
|
|
476
|
+
.description('create a standalone room')
|
|
477
|
+
.requiredOption('--name <name>', 'room name')
|
|
478
|
+
.option('--template <name>', 'room template')
|
|
479
|
+
.option('--goal <text>', 'room goal')
|
|
480
|
+
.option('--brief <text>', 'room briefing')
|
|
481
|
+
.option('--brief-file <path>', 'room briefing from file')
|
|
482
|
+
.option('--json', 'JSON output')
|
|
483
|
+
.action(async (opts) => {
|
|
484
|
+
try {
|
|
485
|
+
const cfg = loadCfg(opts);
|
|
486
|
+
let brief = opts.brief;
|
|
487
|
+
if (opts.briefFile)
|
|
488
|
+
brief = readFileSync(opts.briefFile, 'utf8');
|
|
489
|
+
let templateSnapshot;
|
|
490
|
+
if (opts.template) {
|
|
491
|
+
const t = resolveTemplate(opts.template, allTemplates(cfg));
|
|
492
|
+
if (!t)
|
|
493
|
+
die(new Error(`template not found: ${opts.template}`));
|
|
494
|
+
templateSnapshot = snapshotTemplate(t);
|
|
495
|
+
}
|
|
496
|
+
const record = await provisionRoom(cfg, {
|
|
497
|
+
name: opts.name,
|
|
498
|
+
goal: opts.goal,
|
|
499
|
+
brief,
|
|
500
|
+
template: templateSnapshot,
|
|
501
|
+
});
|
|
502
|
+
if (opts.json) {
|
|
503
|
+
console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
console.log(`Room ${record.room_id} created · ${record.state}`);
|
|
507
|
+
if (templateSnapshot)
|
|
508
|
+
console.log(`Template: ${templateSnapshot.name}@${templateSnapshot.version}`);
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
die(e);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
cOpt(roomCmd.command('list'))
|
|
515
|
+
.description('list rooms')
|
|
516
|
+
.option('--state <state>', 'filter by state (active|provisioning|closing|closed|all)')
|
|
517
|
+
.option('--json', 'JSON output')
|
|
518
|
+
.action(async (opts) => {
|
|
519
|
+
try {
|
|
520
|
+
const cfg = loadCfg(opts);
|
|
521
|
+
let stateFilter;
|
|
522
|
+
if (opts.state && opts.state !== 'all') {
|
|
523
|
+
stateFilter = opts.state;
|
|
524
|
+
}
|
|
525
|
+
const local = new Map(listRoomRecords().map(room => [room.room_id, room]));
|
|
526
|
+
const coworkRooms = await coworkFor(cfg).listRooms();
|
|
527
|
+
const rooms = coworkRooms
|
|
528
|
+
.filter(room => !stateFilter || room.state === stateFilter)
|
|
529
|
+
.map(room => ({ ...room, orchestration: local.get(room.room_id) ?? null }));
|
|
530
|
+
if (opts.json) {
|
|
531
|
+
console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (!rooms.length) {
|
|
535
|
+
console.log('No rooms.');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
for (const r of rooms)
|
|
539
|
+
console.log(`${r.room_id} ${r.state} ${r.room_name}${r.orchestration?.task_id ? ` (task: ${r.orchestration.task_id})` : ''}`);
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
die(e);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
cOpt(roomCmd.command('show <id>'))
|
|
546
|
+
.description('show room details')
|
|
547
|
+
.option('--json', 'JSON output')
|
|
548
|
+
.action(async (id, opts) => {
|
|
549
|
+
try {
|
|
550
|
+
const cfg = loadCfg(opts);
|
|
551
|
+
const cowork = await coworkFor(cfg).getRoom(id);
|
|
552
|
+
if (!cowork)
|
|
553
|
+
die(new Error(`room not found: ${id}`));
|
|
554
|
+
const r = getRoomRecord(id);
|
|
555
|
+
if (opts.json) {
|
|
556
|
+
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
console.log(`Room: ${cowork.room_id}`);
|
|
560
|
+
console.log(`Name: ${cowork.room_name}`);
|
|
561
|
+
console.log(`State: ${cowork.state}`);
|
|
562
|
+
console.log(`Identity CID: ${cowork.identity_cid}`);
|
|
563
|
+
if (r?.task_id)
|
|
564
|
+
console.log(`Task: ${r.task_id}`);
|
|
565
|
+
if (r)
|
|
566
|
+
console.log(`Saga: ${r.saga.phase} (step ${r.saga.step_index})`);
|
|
567
|
+
if (r?.provisioning_detail)
|
|
568
|
+
console.log(`Detail: ${r.provisioning_detail}`);
|
|
569
|
+
if (r?.saga.error)
|
|
570
|
+
console.log(`Error: ${r.saga.error}`);
|
|
571
|
+
if (cowork.seats.length) {
|
|
572
|
+
console.log('Members:');
|
|
573
|
+
for (const s of cowork.seats)
|
|
574
|
+
console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
|
|
575
|
+
}
|
|
576
|
+
if (r)
|
|
577
|
+
console.log(`Tracked by Fleet since: ${r.created_at}`);
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
die(e);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
cOpt(roomCmd.command('members <id>'))
|
|
584
|
+
.description('show room members')
|
|
585
|
+
.option('--json', 'JSON output')
|
|
586
|
+
.action(async (id, opts) => {
|
|
587
|
+
try {
|
|
588
|
+
const cfg = loadCfg(opts);
|
|
589
|
+
const r = getRoomRecord(id);
|
|
590
|
+
const members = await coworkFor(cfg).getSeats(id);
|
|
591
|
+
if (opts.json) {
|
|
592
|
+
console.log(JSON.stringify({
|
|
593
|
+
schema_version: 1,
|
|
594
|
+
room_id: id,
|
|
595
|
+
members,
|
|
596
|
+
owner_seat_cid: r?.owner_seat_cid ?? null,
|
|
597
|
+
}, null, 2));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
console.log(`Room ${id}${r ? ` — ${r.room_name}` : ''}`);
|
|
601
|
+
if (r?.owner_seat_cid)
|
|
602
|
+
console.log(`Owner: ${r.owner_seat_cid}`);
|
|
603
|
+
if (!members.length) {
|
|
604
|
+
console.log('No members.');
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
for (const s of members)
|
|
608
|
+
console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
die(e);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
cOpt(roomCmd.command('close <id> <confirm-id>'))
|
|
615
|
+
.description('close a room (requires ID twice for confirmation)')
|
|
616
|
+
.option('--json', 'JSON output')
|
|
617
|
+
.action(async (id, confirmId, opts) => {
|
|
618
|
+
try {
|
|
619
|
+
if (id !== confirmId)
|
|
620
|
+
die(new Error('confirmation ID must match room ID'));
|
|
621
|
+
const cfg = loadCfg(opts);
|
|
622
|
+
await coworkFor(cfg).closeRoom(id);
|
|
623
|
+
const existing = getRoomRecord(id);
|
|
624
|
+
const r = existing ? closeRoomRecord(id) : undefined;
|
|
625
|
+
if (opts.json) {
|
|
626
|
+
console.log(JSON.stringify({ schema_version: 1, room_id: id, state: 'closed', orchestration: r ?? null }, null, 2));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
console.log(`Room ${id} · closed`);
|
|
630
|
+
}
|
|
631
|
+
catch (e) {
|
|
632
|
+
die(e);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
cOpt(roomCmd.command('recover <id>'))
|
|
636
|
+
.description('attempt to recover a stuck room')
|
|
637
|
+
.option('--json', 'JSON output')
|
|
638
|
+
.action(async (id, opts) => {
|
|
639
|
+
try {
|
|
640
|
+
const cfg = loadCfg(opts);
|
|
641
|
+
const adapter = coworkFor(cfg);
|
|
642
|
+
const cowork = await adapter.recoverRoom(id);
|
|
643
|
+
let r = getRoomRecord(id);
|
|
644
|
+
if (r && !r.owner_seat_cid
|
|
645
|
+
&& (r.provisioning_detail === 'waiting_owner_invite'
|
|
646
|
+
|| r.provisioning_detail === 'owner_cid_mismatch')) {
|
|
647
|
+
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
648
|
+
const existing = (await adapter.getSeats(id))
|
|
649
|
+
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
650
|
+
if (!existing && !cfg.ownerInvite)
|
|
651
|
+
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
652
|
+
let acceptedCid = existing?.identity_cid;
|
|
653
|
+
if (!acceptedCid) {
|
|
654
|
+
const accepted = await adapter.acceptInvite(id, cfg.ownerInvite, {
|
|
655
|
+
role: cfg.rooms.owner.role,
|
|
656
|
+
expected_cid: cfg.rooms.owner.expected_cid,
|
|
657
|
+
});
|
|
658
|
+
acceptedCid = accepted.seat_cid;
|
|
659
|
+
}
|
|
660
|
+
setOwnerSeat(id, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
661
|
+
r = advanceSaga(id, 'create_members', 3);
|
|
662
|
+
}
|
|
663
|
+
const actions = [];
|
|
664
|
+
if (r?.saga.error)
|
|
665
|
+
actions.push(`Last error: ${r.saga.error}`);
|
|
666
|
+
if (r?.saga.recovery_hint)
|
|
667
|
+
actions.push(r.saga.recovery_hint);
|
|
668
|
+
if (r?.provisioning_detail === 'waiting_cowork')
|
|
669
|
+
actions.push('Check ours-cowork service status');
|
|
670
|
+
if (r?.provisioning_detail === 'waiting_owner_invite')
|
|
671
|
+
actions.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
672
|
+
if (opts.json) {
|
|
673
|
+
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
console.log(`Room ${cowork.room_id} · ${cowork.state}${r ? ` · saga: ${r.saga.phase}` : ''}`);
|
|
677
|
+
if (actions.length) {
|
|
678
|
+
console.log('Recovery:');
|
|
679
|
+
for (const a of actions)
|
|
680
|
+
console.log(` - ${a}`);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
console.log('No recovery actions needed.');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
die(e);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|