@ours.network/fleet 1.0.3 → 1.0.4
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/application/role-command-service.d.ts +29 -1
- package/dist/application/role-command-service.js +41 -2
- package/dist/application/role-creation-service.d.ts +32 -1
- package/dist/application/role-creation-service.js +56 -10
- package/dist/application/role-removal-service.d.ts +19 -0
- package/dist/application/role-removal-service.js +13 -3
- package/dist/application/session-mutations.d.ts +7 -0
- package/dist/application/session-mutations.js +8 -0
- package/dist/application/task-room-service.d.ts +227 -0
- package/dist/application/task-room-service.js +529 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +39 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/owner-channel/channel.d.ts +5 -0
- package/dist/owner-channel/channel.js +143 -18
- package/dist/owner-channel/commands.d.ts +26 -1
- package/dist/owner-channel/commands.js +66 -128
- package/dist/rooms-tasks/cli.js +181 -462
- package/dist/runner.js +10 -33
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +22 -21
- package/dist/watchdog/query.d.ts +2 -0
- package/dist/watchdog/query.js +7 -3
- package/dist/web/server.js +2 -2
- package/package.json +1 -1
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { loadConfig, ConfigError } from '../config.js';
|
|
3
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
4
|
import { launchFleetWorker } from './external-worker.js';
|
|
7
|
-
import { resolveTemplate,
|
|
8
|
-
import {
|
|
9
|
-
import { createRoomRecord, getRoomRecord,
|
|
10
|
-
import { createCoworkAdapter, CoworkProtocolError
|
|
11
|
-
import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from './types.js';
|
|
5
|
+
import { resolveTemplate, snapshotTemplate, } from './templates.js';
|
|
6
|
+
import { getTask, activateTask, TaskStateError, } from './task-state.js';
|
|
7
|
+
import { createRoomRecord, getRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
|
|
8
|
+
import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
|
|
12
9
|
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
|
|
10
|
+
import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
13
11
|
class TaskRoomPublicError extends Error {
|
|
14
12
|
code;
|
|
15
13
|
fields;
|
|
@@ -119,6 +117,8 @@ function isKnownRoomStateMessage(message) {
|
|
|
119
117
|
].some(pattern => pattern.test(message));
|
|
120
118
|
}
|
|
121
119
|
function dieTaskRoom(e) {
|
|
120
|
+
if (e instanceof TaskRoomApplicationError)
|
|
121
|
+
e = taskRoomPublicError(e.code, e.fields);
|
|
122
122
|
if (e instanceof TaskRoomPublicError) {
|
|
123
123
|
const failure = taskRoomPublicFailure(e);
|
|
124
124
|
const output = renderMarkdownFailure({
|
|
@@ -186,7 +186,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
|
|
|
186
186
|
await launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath);
|
|
187
187
|
}
|
|
188
188
|
catch (error) {
|
|
189
|
-
await
|
|
189
|
+
await taskRoomService(configPath).recordSettlementError({
|
|
190
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
|
|
191
|
+
recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
|
|
192
|
+
});
|
|
190
193
|
throw error;
|
|
191
194
|
}
|
|
192
195
|
const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
|
|
@@ -207,7 +210,10 @@ async function launchRoomDeleteWorker(roomId, configPath) {
|
|
|
207
210
|
await launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath);
|
|
208
211
|
}
|
|
209
212
|
catch (error) {
|
|
210
|
-
await
|
|
213
|
+
await taskRoomService(configPath).recordRoomSettlementError({
|
|
214
|
+
actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
|
|
215
|
+
recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
|
|
216
|
+
});
|
|
211
217
|
throw error;
|
|
212
218
|
}
|
|
213
219
|
const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
|
|
@@ -225,6 +231,14 @@ async function launchRoomDeleteWorker(roomId, configPath) {
|
|
|
225
231
|
function loadCfg(opts) {
|
|
226
232
|
return loadConfig(opts.configuration);
|
|
227
233
|
}
|
|
234
|
+
function taskRoomService(configuration) {
|
|
235
|
+
return new TaskRoomApplicationService(configuration, {
|
|
236
|
+
loadConfiguration: loadConfig,
|
|
237
|
+
cowork: coworkFor,
|
|
238
|
+
binPath: getBinPath,
|
|
239
|
+
provisionMembers,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
228
242
|
function allTemplates(cfg) {
|
|
229
243
|
return cfg.roomTemplates ?? {};
|
|
230
244
|
}
|
|
@@ -344,8 +358,7 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
344
358
|
.option('--json', 'JSON output')
|
|
345
359
|
.action((opts) => {
|
|
346
360
|
try {
|
|
347
|
-
const
|
|
348
|
-
const templates = listTemplates(allTemplates(cfg));
|
|
361
|
+
const templates = taskRoomService(opts.configuration).listTemplates();
|
|
349
362
|
if (opts.json) {
|
|
350
363
|
console.log(JSON.stringify({ schema_version: 1, templates }, null, 2));
|
|
351
364
|
return;
|
|
@@ -368,12 +381,9 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
368
381
|
.option('--json', 'JSON output')
|
|
369
382
|
.action((name, opts) => {
|
|
370
383
|
try {
|
|
371
|
-
const
|
|
372
|
-
const t = resolveTemplate(name, allTemplates(cfg));
|
|
373
|
-
if (!t)
|
|
374
|
-
die(new Error(`template not found: ${name}`));
|
|
384
|
+
const t = taskRoomService(opts.configuration).getTemplate(name);
|
|
375
385
|
if (opts.json) {
|
|
376
|
-
console.log(JSON.stringify({ schema_version: 1, template:
|
|
386
|
+
console.log(JSON.stringify({ schema_version: 1, template: t }, null, 2));
|
|
377
387
|
return;
|
|
378
388
|
}
|
|
379
389
|
console.log(`${t.name}@${t.version} ${t.description}`);
|
|
@@ -382,7 +392,7 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
382
392
|
console.log(`\nMembers:`);
|
|
383
393
|
for (const m of t.members)
|
|
384
394
|
console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
|
|
385
|
-
console.log(`\nContent hash: ${
|
|
395
|
+
console.log(`\nContent hash: ${t.content_hash}`);
|
|
386
396
|
}
|
|
387
397
|
catch (e) {
|
|
388
398
|
die(e);
|
|
@@ -393,18 +403,7 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
393
403
|
.option('--json', 'JSON output')
|
|
394
404
|
.action((opts) => {
|
|
395
405
|
try {
|
|
396
|
-
const
|
|
397
|
-
const templates = listTemplates(allTemplates(cfg));
|
|
398
|
-
const problems = [];
|
|
399
|
-
for (const t of templates) {
|
|
400
|
-
const issues = [];
|
|
401
|
-
for (const m of t.members) {
|
|
402
|
-
if (!cfg.roles.some(r => r.name === m.role_ref))
|
|
403
|
-
issues.push(`member ${m.slot}: role_ref '${m.role_ref}' not found in fleet roles`);
|
|
404
|
-
}
|
|
405
|
-
if (issues.length)
|
|
406
|
-
problems.push({ template: `${t.name}@${t.version}`, issues });
|
|
407
|
-
}
|
|
406
|
+
const problems = taskRoomService(opts.configuration).validateTemplates();
|
|
408
407
|
if (opts.json) {
|
|
409
408
|
console.log(JSON.stringify({ schema_version: 1, valid: !problems.length, problems }, null, 2));
|
|
410
409
|
return;
|
|
@@ -439,69 +438,24 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
439
438
|
.option('--json', 'JSON output')
|
|
440
439
|
.action(async (opts) => {
|
|
441
440
|
try {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
throw taskRoomPublicError('template_not_found', { template: opts.template });
|
|
448
|
-
const snap = snapshotTemplate(t);
|
|
449
|
-
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
450
|
-
}
|
|
451
|
-
else if (opts.room !== false) {
|
|
452
|
-
const defaultTpl = cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'team';
|
|
453
|
-
const t = resolveTemplate(defaultTpl, allTemplates(cfg));
|
|
454
|
-
if (t) {
|
|
455
|
-
const snap = snapshotTemplate(t);
|
|
456
|
-
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
let brief = opts.brief;
|
|
460
|
-
if (opts.briefFile) {
|
|
461
|
-
brief = readFileSync(opts.briefFile, 'utf8');
|
|
462
|
-
}
|
|
463
|
-
const origin = { type: 'cli' };
|
|
464
|
-
let record = createTask({
|
|
465
|
-
title: opts.title,
|
|
466
|
-
brief,
|
|
467
|
-
brief_file: opts.briefFile,
|
|
468
|
-
template: templateRef,
|
|
469
|
-
origin,
|
|
470
|
-
idempotency_key: opts.idempotencyKey,
|
|
471
|
-
start: !opts.backlog,
|
|
441
|
+
const record = await taskRoomService(opts.configuration).createTask({
|
|
442
|
+
actor: { kind: 'local_control', surface: 'cli' }, title: opts.title,
|
|
443
|
+
brief: opts.brief, briefFile: opts.briefFile, template: opts.template,
|
|
444
|
+
backlog: opts.backlog, noRoom: opts.room === false,
|
|
445
|
+
idempotencyKey: opts.idempotencyKey, origin: { type: 'cli' },
|
|
472
446
|
});
|
|
473
|
-
if (!opts.backlog && opts.room !== false && templateRef && !record.room_id) {
|
|
474
|
-
const template = resolveRoomTemplate(cfg, templateRef.name);
|
|
475
|
-
try {
|
|
476
|
-
await provisionRoom(cfg, {
|
|
477
|
-
name: opts.title,
|
|
478
|
-
goal: opts.title,
|
|
479
|
-
brief,
|
|
480
|
-
template,
|
|
481
|
-
taskId: record.task_id,
|
|
482
|
-
onCreated: room => {
|
|
483
|
-
record = updateTaskRoom(record.task_id, room.room_id, room.room_identity_cid);
|
|
484
|
-
},
|
|
485
|
-
});
|
|
486
|
-
record = getTask(record.task_id);
|
|
487
|
-
}
|
|
488
|
-
catch (error) {
|
|
489
|
-
if (error instanceof CoworkUnavailableError) {
|
|
490
|
-
blockTask(record.task_id, 'Cowork management socket is unavailable');
|
|
491
|
-
}
|
|
492
|
-
throw error;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
447
|
if (opts.json) {
|
|
496
448
|
console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
|
|
497
449
|
return;
|
|
498
450
|
}
|
|
499
451
|
console.log(taskActionMarkdown('Task created', record, [
|
|
500
452
|
{ label: 'Title', value: record.title },
|
|
501
|
-
...(
|
|
453
|
+
...(record.template ? [{ label: 'Template', value: `${record.template.name}@${record.template.version}`, kind: 'code' }] : []),
|
|
502
454
|
]));
|
|
503
455
|
}
|
|
504
456
|
catch (e) {
|
|
457
|
+
if (e instanceof TaskRoomApplicationError && e.code === 'template_not_found')
|
|
458
|
+
e = taskRoomPublicError('template_not_found', { template: opts.template });
|
|
505
459
|
if (opts.json)
|
|
506
460
|
die(e);
|
|
507
461
|
dieTaskRoom(e);
|
|
@@ -517,7 +471,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
517
471
|
if (opts.state && opts.state !== 'all') {
|
|
518
472
|
stateFilter = opts.state;
|
|
519
473
|
}
|
|
520
|
-
const tasks = listTasks(stateFilter ? { state: stateFilter } : undefined);
|
|
474
|
+
const tasks = taskRoomService(opts.configuration).listTasks(stateFilter ? { state: stateFilter } : undefined);
|
|
521
475
|
if (opts.json) {
|
|
522
476
|
console.log(JSON.stringify({ schema_version: 1, tasks }, null, 2));
|
|
523
477
|
return;
|
|
@@ -535,8 +489,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
535
489
|
.option('--json', 'JSON output')
|
|
536
490
|
.action((id, opts) => {
|
|
537
491
|
try {
|
|
538
|
-
const t = getTask(id);
|
|
539
|
-
const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
492
|
+
const { task: t, orchestration: room } = taskRoomService().getTask(id);
|
|
540
493
|
if (opts.json) {
|
|
541
494
|
console.log(JSON.stringify({
|
|
542
495
|
schema_version: 1, task: t, orchestration: room ?? null,
|
|
@@ -578,26 +531,9 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
578
531
|
.option('--json', 'JSON output')
|
|
579
532
|
.action(async (id, opts) => {
|
|
580
533
|
try {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const template = resolveRoomTemplate(cfg, t.template.name);
|
|
585
|
-
if (!template || template.content_hash !== t.template.content_hash)
|
|
586
|
-
throw taskRoomPublicError('task_template_drift', {
|
|
587
|
-
template: `${t.template.name}@${t.template.version}`,
|
|
588
|
-
});
|
|
589
|
-
await provisionRoom(cfg, {
|
|
590
|
-
name: t.title,
|
|
591
|
-
goal: t.title,
|
|
592
|
-
brief: t.brief,
|
|
593
|
-
template,
|
|
594
|
-
taskId: t.task_id,
|
|
595
|
-
onCreated: room => {
|
|
596
|
-
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
597
|
-
},
|
|
598
|
-
});
|
|
599
|
-
t = getTask(t.task_id);
|
|
600
|
-
}
|
|
534
|
+
const t = await taskRoomService(opts.configuration).startTask({
|
|
535
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
536
|
+
});
|
|
601
537
|
if (opts.json) {
|
|
602
538
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
603
539
|
return;
|
|
@@ -605,6 +541,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
605
541
|
console.log(taskActionMarkdown('Task started', t));
|
|
606
542
|
}
|
|
607
543
|
catch (e) {
|
|
544
|
+
if (e instanceof TaskRoomApplicationError && e.code === 'task_template_drift')
|
|
545
|
+
e = taskRoomPublicError('task_template_drift', {
|
|
546
|
+
template: getTask(id).template
|
|
547
|
+
? `${getTask(id).template.name}@${getTask(id).template.version}` : undefined,
|
|
548
|
+
});
|
|
608
549
|
if (opts.json)
|
|
609
550
|
die(e);
|
|
610
551
|
dieTaskRoom(e);
|
|
@@ -616,7 +557,9 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
616
557
|
.option('--json', 'JSON output')
|
|
617
558
|
.action((id, opts) => {
|
|
618
559
|
try {
|
|
619
|
-
const t = blockTask(
|
|
560
|
+
const t = taskRoomService().blockTask({
|
|
561
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
|
|
562
|
+
});
|
|
620
563
|
if (opts.json) {
|
|
621
564
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
622
565
|
return;
|
|
@@ -634,7 +577,9 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
634
577
|
.option('--json', 'JSON output')
|
|
635
578
|
.action((id, opts) => {
|
|
636
579
|
try {
|
|
637
|
-
const t = unblockTask(
|
|
580
|
+
const t = taskRoomService().unblockTask({
|
|
581
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
582
|
+
});
|
|
638
583
|
if (opts.json) {
|
|
639
584
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
640
585
|
return;
|
|
@@ -652,7 +597,9 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
652
597
|
.option('--json', 'JSON output')
|
|
653
598
|
.action((id, opts) => {
|
|
654
599
|
try {
|
|
655
|
-
const t = reviewTask(
|
|
600
|
+
const t = taskRoomService().reviewTask({
|
|
601
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
602
|
+
});
|
|
656
603
|
if (opts.json) {
|
|
657
604
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
658
605
|
return;
|
|
@@ -676,25 +623,12 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
676
623
|
if (opts.summaryFile)
|
|
677
624
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
678
625
|
const outcome = summary ? { summary } : undefined;
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
throw new TaskStateError(`cannot transition from '${current.state}' to 'done'`);
|
|
684
|
-
let roomId;
|
|
685
|
-
let cfg;
|
|
686
|
-
if (current.room_id) {
|
|
687
|
-
cfg = loadCfg(opts);
|
|
688
|
-
const shouldClose = cfg.tasks?.close_room_on_done
|
|
689
|
-
?? cfg.rooms?.defaults?.close_when_task_done
|
|
690
|
-
?? false;
|
|
691
|
-
if (shouldClose)
|
|
692
|
-
roomId = current.room_id;
|
|
693
|
-
}
|
|
694
|
-
await acceptTaskTerminalIntent({ taskId: id, kind: 'done', roomId, outcome });
|
|
695
|
-
const settled = roomId
|
|
626
|
+
const plan = await taskRoomService(opts.configuration).completeTask({
|
|
627
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
|
|
628
|
+
});
|
|
629
|
+
const settled = plan.settlementRequired
|
|
696
630
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
697
|
-
: { task:
|
|
631
|
+
: { task: plan.task, timedOut: false };
|
|
698
632
|
const t = settled.task;
|
|
699
633
|
if (opts.json) {
|
|
700
634
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
@@ -723,17 +657,12 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
723
657
|
try {
|
|
724
658
|
if (id !== confirmId)
|
|
725
659
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
throw new TaskStateError(`cannot cancel a '${current.state}' task`);
|
|
729
|
-
if (current.room_id)
|
|
730
|
-
loadCfg(opts);
|
|
731
|
-
await acceptTaskTerminalIntent({
|
|
732
|
-
taskId: id, kind: 'cancelled', roomId: current.room_id,
|
|
660
|
+
const plan = await taskRoomService(opts.configuration).cancelTask({
|
|
661
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
733
662
|
});
|
|
734
|
-
const settled =
|
|
663
|
+
const settled = plan.settlementRequired
|
|
735
664
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
736
|
-
: { task:
|
|
665
|
+
: { task: plan.task, timedOut: false };
|
|
737
666
|
const t = settled.task;
|
|
738
667
|
if (opts.json) {
|
|
739
668
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
@@ -762,7 +691,9 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
762
691
|
try {
|
|
763
692
|
if (id !== confirmId)
|
|
764
693
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
765
|
-
const deleted = deleteTask(
|
|
694
|
+
const deleted = taskRoomService().deleteTask({
|
|
695
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
696
|
+
});
|
|
766
697
|
if (opts.json) {
|
|
767
698
|
console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
|
|
768
699
|
return;
|
|
@@ -783,61 +714,35 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
783
714
|
.option('--json', 'JSON output')
|
|
784
715
|
.action(async (id, opts) => {
|
|
785
716
|
try {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (resumable.includes(room.saga.phase)) {
|
|
816
|
-
const template = durableTaskRoomTemplate(t, room);
|
|
817
|
-
if (template) {
|
|
818
|
-
try {
|
|
819
|
-
await provisionMembers({
|
|
820
|
-
cfg,
|
|
821
|
-
cowork: coworkFor(cfg),
|
|
822
|
-
roomId: room.room_id,
|
|
823
|
-
taskId: t.task_id,
|
|
824
|
-
template,
|
|
825
|
-
binPath: getBinPath(),
|
|
826
|
-
brief: t.brief,
|
|
827
|
-
goal: t.title,
|
|
828
|
-
});
|
|
829
|
-
t = getTask(t.task_id);
|
|
830
|
-
room = getRoomRecord(room.room_id);
|
|
831
|
-
result.task = t;
|
|
832
|
-
result.room = room ?? null;
|
|
833
|
-
result.recovery_actions = ['Provisioning resumed successfully'];
|
|
834
|
-
}
|
|
835
|
-
catch (error) {
|
|
836
|
-
result.recovery_actions.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
717
|
+
const app = taskRoomService(opts.configuration);
|
|
718
|
+
const actor = { kind: 'local_control', surface: 'cli' };
|
|
719
|
+
const begin = await app.beginTaskRecovery({ actor, taskId: id });
|
|
720
|
+
const recovered = begin.kind === 'terminal_worker_required'
|
|
721
|
+
? await (async () => {
|
|
722
|
+
const settled = await launchTaskSettleWorker(id, opts.configuration);
|
|
723
|
+
return app.continueTaskRecovery({ actor, taskId: id, terminalTimedOut: settled.timedOut });
|
|
724
|
+
})()
|
|
725
|
+
: begin.result;
|
|
726
|
+
const t = recovered.task;
|
|
727
|
+
const room = recovered.room;
|
|
728
|
+
const recoveryActions = recovered.issues.map(issue => {
|
|
729
|
+
if (issue.code === 'terminal_pending')
|
|
730
|
+
return `Terminal intent remains pending — retry task recover ${id}`;
|
|
731
|
+
if (issue.code === 'waiting_cowork')
|
|
732
|
+
return 'Cowork management socket unreachable — check ours-cowork service';
|
|
733
|
+
if (issue.code === 'waiting_owner_invite')
|
|
734
|
+
return 'Owner invite invalid or expired — rotate rooms.owner.public_invite in config';
|
|
735
|
+
if (issue.code === 'owner_cid_mismatch')
|
|
736
|
+
return 'Owner CID mismatch — verify rooms.owner.expected_cid matches Messenger identity';
|
|
737
|
+
if (issue.code === 'member_failed')
|
|
738
|
+
return `Member creation failed at saga step ${issue.stepIndex} — inspect and retry`;
|
|
739
|
+
if (issue.code === 'waiting_seats')
|
|
740
|
+
return 'Members have not accepted their one-time room invites yet — inspect role logs, then retry recovery';
|
|
741
|
+
if (issue.code === 'resume_failed')
|
|
742
|
+
return `Resume failed: ${issue.error}`;
|
|
743
|
+
return 'Provisioning resumed successfully';
|
|
744
|
+
});
|
|
745
|
+
const result = { task: t, room: room ?? null, recovery_actions: recoveryActions };
|
|
841
746
|
if (opts.json) {
|
|
842
747
|
console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
|
|
843
748
|
return;
|
|
@@ -853,8 +758,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
853
758
|
{ label: 'Saga', value: room.saga.phase, kind: 'code' },
|
|
854
759
|
] : []),
|
|
855
760
|
],
|
|
856
|
-
sections:
|
|
857
|
-
? [{ heading: 'Next steps', items:
|
|
761
|
+
sections: recoveryActions.length
|
|
762
|
+
? [{ heading: 'Next steps', items: recoveryActions }]
|
|
858
763
|
: [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
|
|
859
764
|
}));
|
|
860
765
|
}
|
|
@@ -869,8 +774,10 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
869
774
|
.option('--json', 'JSON output')
|
|
870
775
|
.action(async (id, opts) => {
|
|
871
776
|
try {
|
|
872
|
-
const
|
|
873
|
-
const t = await
|
|
777
|
+
const app = taskRoomService(opts.configuration);
|
|
778
|
+
const t = await app.settleTask({
|
|
779
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
780
|
+
});
|
|
874
781
|
if (opts.json) {
|
|
875
782
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
876
783
|
return;
|
|
@@ -878,7 +785,41 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
878
785
|
console.log(taskActionMarkdown('Task terminal action settled', t));
|
|
879
786
|
}
|
|
880
787
|
catch (e) {
|
|
881
|
-
await
|
|
788
|
+
await taskRoomService(opts.configuration).recordSettlementError({
|
|
789
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
790
|
+
error: errorText(e), recoveryHint: `External settle worker failed. Retry task recover ${id}.`,
|
|
791
|
+
}).catch(() => { });
|
|
792
|
+
if (opts.json)
|
|
793
|
+
die(e);
|
|
794
|
+
dieTaskRoom(e);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
cOpt(taskCmd.command('_recover <id>', { hidden: true }))
|
|
798
|
+
.description('internal: settle and continue task recovery')
|
|
799
|
+
.option('--json', 'JSON output')
|
|
800
|
+
.action(async (id, opts) => {
|
|
801
|
+
const app = taskRoomService(opts.configuration);
|
|
802
|
+
const actor = { kind: 'internal_worker', surface: 'cli' };
|
|
803
|
+
try {
|
|
804
|
+
const begin = await app.beginTaskRecovery({ actor, taskId: id });
|
|
805
|
+
const result = begin.kind === 'terminal_worker_required'
|
|
806
|
+
? await (async () => {
|
|
807
|
+
try {
|
|
808
|
+
await app.settleTask({ actor, taskId: id });
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
await app.recordSettlementError({
|
|
812
|
+
actor, taskId: id, error: errorText(error),
|
|
813
|
+
recoveryHint: `External settle worker failed. Retry task recover ${id}.`,
|
|
814
|
+
}).catch(() => { });
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
return app.continueTaskRecovery({ actor, taskId: id, terminalTimedOut: false });
|
|
818
|
+
})()
|
|
819
|
+
: begin.result;
|
|
820
|
+
console.log(JSON.stringify({ schema_version: 1, recovery: result }, null, 2));
|
|
821
|
+
}
|
|
822
|
+
catch (e) {
|
|
882
823
|
if (opts.json)
|
|
883
824
|
die(e);
|
|
884
825
|
dieTaskRoom(e);
|
|
@@ -890,25 +831,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
890
831
|
.option('--json', 'JSON output')
|
|
891
832
|
.action(async (id, opts) => {
|
|
892
833
|
try {
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (
|
|
898
|
-
// An explicit --template must agree with the room the task already
|
|
899
|
-
// runs in; a conflicting override is an error, not a silent no-op.
|
|
900
|
-
if (opts.template) {
|
|
901
|
-
const override = resolveTemplate(opts.template, allTemplates(cfg));
|
|
902
|
-
if (!override)
|
|
903
|
-
throw taskRoomPublicError('template_not_found', { template: opts.template });
|
|
904
|
-
const overrideSnap = snapshotTemplate(override);
|
|
905
|
-
const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
|
|
906
|
-
if (roomSnap && (roomSnap.name !== overrideSnap.name || roomSnap.content_hash !== overrideSnap.content_hash))
|
|
907
|
-
throw taskRoomPublicError('template_mismatch', {
|
|
908
|
-
requested: `${overrideSnap.name}@${overrideSnap.version}`, room: t.room_id,
|
|
909
|
-
provisioned: `${roomSnap.name}@${roomSnap.version}`,
|
|
910
|
-
});
|
|
911
|
-
}
|
|
834
|
+
const result = await taskRoomService(opts.configuration).ensureTaskWork({
|
|
835
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, template: opts.template,
|
|
836
|
+
});
|
|
837
|
+
const t = result.task;
|
|
838
|
+
if (result.status === 'already_active') {
|
|
912
839
|
if (opts.json) {
|
|
913
840
|
console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
|
|
914
841
|
return;
|
|
@@ -916,107 +843,6 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
916
843
|
console.log(taskActionMarkdown('Task already active', t, t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []));
|
|
917
844
|
return;
|
|
918
845
|
}
|
|
919
|
-
// Select and validate the template snapshot before any state change or
|
|
920
|
-
// provisioning. An explicit --template re-pins the task; a stored ref
|
|
921
|
-
// must still match its snapshot (same contract as `task start`).
|
|
922
|
-
const existingRoom = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
923
|
-
const durableSnapshot = existingRoom ? durableTaskRoomTemplate(t, existingRoom) : undefined;
|
|
924
|
-
const templateName = opts.template
|
|
925
|
-
?? durableSnapshot?.name
|
|
926
|
-
?? t.template?.name
|
|
927
|
-
?? cfg.tasks?.default_room_template
|
|
928
|
-
?? cfg.rooms?.defaults?.template
|
|
929
|
-
?? 'single';
|
|
930
|
-
const resolved = durableSnapshot && !opts.template
|
|
931
|
-
? undefined : resolveTemplate(templateName, allTemplates(cfg));
|
|
932
|
-
if (!durableSnapshot && !resolved)
|
|
933
|
-
throw taskRoomPublicError('template_not_found', { template: templateName });
|
|
934
|
-
if (opts.template && !resolved)
|
|
935
|
-
throw taskRoomPublicError('template_not_found', { template: templateName });
|
|
936
|
-
if (durableSnapshot && resolved) {
|
|
937
|
-
const requested = snapshotTemplate(resolved);
|
|
938
|
-
if (requested.name !== durableSnapshot.name
|
|
939
|
-
|| requested.content_hash !== durableSnapshot.content_hash) {
|
|
940
|
-
throw taskRoomPublicError('template_mismatch', {
|
|
941
|
-
requested: `${requested.name}@${requested.version}`, room: existingRoom.room_id,
|
|
942
|
-
provisioned: `${durableSnapshot.name}@${durableSnapshot.version}`,
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
const snap = durableSnapshot ?? snapshotTemplate(resolved);
|
|
947
|
-
if (!opts.template && t.template && snap.content_hash !== t.template.content_hash)
|
|
948
|
-
throw taskRoomPublicError('task_template_drift', {
|
|
949
|
-
template: `${t.template.name}@${t.template.version}`,
|
|
950
|
-
});
|
|
951
|
-
// A provisioned room is pinned to its snapshot: never resume or re-pin
|
|
952
|
-
// an existing room under a different template.
|
|
953
|
-
if (t.room_id) {
|
|
954
|
-
const roomSnap = getRoomRecord(t.room_id)?.template_snapshot;
|
|
955
|
-
if (roomSnap && (roomSnap.name !== snap.name || roomSnap.content_hash !== snap.content_hash))
|
|
956
|
-
throw taskRoomPublicError('template_mismatch', {
|
|
957
|
-
requested: `${snap.name}@${snap.version}`, room: t.room_id,
|
|
958
|
-
provisioned: `${roomSnap.name}@${roomSnap.version}`,
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
let templateRef = t.template;
|
|
962
|
-
if (!templateRef || templateRef.name !== snap.name || templateRef.content_hash !== snap.content_hash) {
|
|
963
|
-
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
964
|
-
t = updateTaskTemplate(t.task_id, templateRef);
|
|
965
|
-
}
|
|
966
|
-
if (t.state === 'backlog') {
|
|
967
|
-
t = startTask(id);
|
|
968
|
-
}
|
|
969
|
-
if (!t.room_id) {
|
|
970
|
-
try {
|
|
971
|
-
await provisionRoom(cfg, {
|
|
972
|
-
name: t.title,
|
|
973
|
-
goal: t.title,
|
|
974
|
-
brief: t.brief,
|
|
975
|
-
template: snap,
|
|
976
|
-
taskId: t.task_id,
|
|
977
|
-
onCreated: room => {
|
|
978
|
-
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
979
|
-
},
|
|
980
|
-
});
|
|
981
|
-
t = getTask(t.task_id);
|
|
982
|
-
}
|
|
983
|
-
catch (error) {
|
|
984
|
-
if (error instanceof CoworkUnavailableError) {
|
|
985
|
-
blockTask(t.task_id, 'Cowork management socket is unavailable');
|
|
986
|
-
}
|
|
987
|
-
throw error;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
else if (t.state === 'provisioning') {
|
|
991
|
-
// The task already has a room mid-provisioning (e.g. seats were still
|
|
992
|
-
// pending on the last run). Resume it exactly as `task recover` does.
|
|
993
|
-
const room = getRoomRecord(t.room_id);
|
|
994
|
-
const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
|
|
995
|
-
if (room && resumable.includes(room.saga.phase)) {
|
|
996
|
-
try {
|
|
997
|
-
await provisionMembers({
|
|
998
|
-
cfg,
|
|
999
|
-
cowork: coworkFor(cfg),
|
|
1000
|
-
roomId: room.room_id,
|
|
1001
|
-
taskId: t.task_id,
|
|
1002
|
-
template: snap,
|
|
1003
|
-
binPath: getBinPath(),
|
|
1004
|
-
brief: t.brief,
|
|
1005
|
-
goal: t.title,
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
catch (error) {
|
|
1009
|
-
if (error instanceof CoworkUnavailableError) {
|
|
1010
|
-
blockTask(t.task_id, 'Cowork management socket is unavailable');
|
|
1011
|
-
}
|
|
1012
|
-
throw error;
|
|
1013
|
-
}
|
|
1014
|
-
t = getTask(t.task_id);
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
throw taskRoomPublicError('task_non_resumable', { task: id, room: t.room_id });
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
846
|
if (opts.json) {
|
|
1021
847
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
1022
848
|
return;
|
|
@@ -1026,15 +852,15 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1026
852
|
fields: [
|
|
1027
853
|
{ label: 'ID', value: t.task_id, kind: 'code' },
|
|
1028
854
|
{ label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
|
|
1029
|
-
...(
|
|
855
|
+
...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
|
|
1030
856
|
...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
|
|
1031
857
|
],
|
|
1032
|
-
sections: t.member_roles.length ? [{
|
|
1033
|
-
heading: 'Agents', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)}`),
|
|
1034
|
-
}] : [],
|
|
858
|
+
sections: t.member_roles.length ? [{ heading: 'Agents', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)}`) }] : [],
|
|
1035
859
|
}));
|
|
1036
860
|
}
|
|
1037
861
|
catch (e) {
|
|
862
|
+
if (e instanceof TaskRoomApplicationError)
|
|
863
|
+
e = taskRoomPublicError(e.code, e.fields);
|
|
1038
864
|
if (opts.json)
|
|
1039
865
|
die(e);
|
|
1040
866
|
dieTaskRoom(e);
|
|
@@ -1051,19 +877,13 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1051
877
|
if (opts.summaryFile)
|
|
1052
878
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
1053
879
|
const outcome = summary ? { summary } : undefined;
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
reviewTask(id);
|
|
1059
|
-
}
|
|
1060
|
-
if (t.room_id)
|
|
1061
|
-
loadCfg(opts); // validate configuration before durable acceptance
|
|
1062
|
-
await acceptTaskTerminalIntent({ taskId: id, kind: 'done', roomId: t.room_id, outcome });
|
|
1063
|
-
const settled = t.room_id
|
|
880
|
+
const plan = await taskRoomService(opts.configuration).finishTask({
|
|
881
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
|
|
882
|
+
});
|
|
883
|
+
const settled = plan.settlementRequired
|
|
1064
884
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
1065
|
-
: { task:
|
|
1066
|
-
t = settled.task;
|
|
885
|
+
: { task: plan.task, timedOut: false };
|
|
886
|
+
const t = settled.task;
|
|
1067
887
|
if (opts.json) {
|
|
1068
888
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
1069
889
|
return;
|
|
@@ -1079,6 +899,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1079
899
|
console.log(taskActionMarkdown('Task finished', t, t.outcome ? [{ label: 'Summary', value: t.outcome.summary, multiline: true }] : []));
|
|
1080
900
|
}
|
|
1081
901
|
catch (e) {
|
|
902
|
+
if (e instanceof TaskRoomApplicationError)
|
|
903
|
+
e = taskRoomPublicError(e.code, e.fields);
|
|
1082
904
|
if (opts.json)
|
|
1083
905
|
die(e);
|
|
1084
906
|
dieTaskRoom(e);
|
|
@@ -1097,22 +919,9 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1097
919
|
.option('--json', 'JSON output')
|
|
1098
920
|
.action(async (opts) => {
|
|
1099
921
|
try {
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
brief = readFileSync(opts.briefFile, 'utf8');
|
|
1104
|
-
let templateSnapshot;
|
|
1105
|
-
if (opts.template) {
|
|
1106
|
-
const t = resolveTemplate(opts.template, allTemplates(cfg));
|
|
1107
|
-
if (!t)
|
|
1108
|
-
throw taskRoomPublicError('template_not_found', { template: opts.template });
|
|
1109
|
-
templateSnapshot = snapshotTemplate(t);
|
|
1110
|
-
}
|
|
1111
|
-
const record = await provisionRoom(cfg, {
|
|
1112
|
-
name: opts.name,
|
|
1113
|
-
goal: opts.goal,
|
|
1114
|
-
brief,
|
|
1115
|
-
template: templateSnapshot,
|
|
922
|
+
const record = await taskRoomService(opts.configuration).createRoom({
|
|
923
|
+
actor: { kind: 'local_control', surface: 'cli' }, name: opts.name,
|
|
924
|
+
template: opts.template, goal: opts.goal, brief: opts.brief, briefFile: opts.briefFile,
|
|
1116
925
|
});
|
|
1117
926
|
if (opts.json) {
|
|
1118
927
|
console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
|
|
@@ -1120,7 +929,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1120
929
|
}
|
|
1121
930
|
console.log(roomActionMarkdown('Room created', record, [
|
|
1122
931
|
{ label: 'Name', value: record.room_name },
|
|
1123
|
-
...(
|
|
932
|
+
...(record.template_snapshot ? [{ label: 'Template', value: `${record.template_snapshot.name}@${record.template_snapshot.version}`, kind: 'code' }] : []),
|
|
1124
933
|
]));
|
|
1125
934
|
}
|
|
1126
935
|
catch (e) {
|
|
@@ -1135,23 +944,11 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1135
944
|
.option('--json', 'JSON output')
|
|
1136
945
|
.action(async (opts) => {
|
|
1137
946
|
try {
|
|
1138
|
-
const cfg = loadCfg(opts);
|
|
1139
947
|
if (opts.state && !['active', 'provisioning', 'all'].includes(opts.state))
|
|
1140
948
|
throw taskRoomPublicError('room_filter');
|
|
1141
949
|
const stateFilter = opts.state === 'active' || opts.state === 'provisioning'
|
|
1142
950
|
? opts.state : undefined;
|
|
1143
|
-
const
|
|
1144
|
-
await deleteLegacyClosedRooms({ cowork: adapter });
|
|
1145
|
-
const local = new Map(listRoomRecords().map(room => [room.room_id, room]));
|
|
1146
|
-
const coworkRooms = await adapter.listRooms();
|
|
1147
|
-
const rooms = coworkRooms
|
|
1148
|
-
.filter(room => room.state === 'active' || room.state === 'provisioning')
|
|
1149
|
-
.filter(room => {
|
|
1150
|
-
const tracked = local.get(room.room_id);
|
|
1151
|
-
return !tracked || tracked.state === 'active' || tracked.state === 'provisioning';
|
|
1152
|
-
})
|
|
1153
|
-
.filter(room => !stateFilter || room.state === stateFilter)
|
|
1154
|
-
.map(room => ({ ...room, orchestration: local.get(room.room_id) ?? null }));
|
|
951
|
+
const rooms = await taskRoomService(opts.configuration).listRooms(stateFilter ? { state: stateFilter } : undefined);
|
|
1155
952
|
if (opts.json) {
|
|
1156
953
|
console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
|
|
1157
954
|
return;
|
|
@@ -1173,14 +970,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1173
970
|
.option('--json', 'JSON output')
|
|
1174
971
|
.action(async (id, opts) => {
|
|
1175
972
|
try {
|
|
1176
|
-
const
|
|
1177
|
-
const tracked = getRoomRecord(id);
|
|
1178
|
-
if (tracked?.state === 'closing' || tracked?.state === 'closed')
|
|
1179
|
-
throw taskRoomPublicError('room_not_found', { room: id });
|
|
1180
|
-
const cowork = await coworkFor(cfg).getRoom(id);
|
|
1181
|
-
if (!cowork || cowork.state === 'closing' || cowork.state === 'closed')
|
|
1182
|
-
throw taskRoomPublicError('room_not_found', { room: id });
|
|
1183
|
-
const r = tracked;
|
|
973
|
+
const { room: cowork, orchestration: r } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1184
974
|
if (opts.json) {
|
|
1185
975
|
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
|
|
1186
976
|
return;
|
|
@@ -1217,13 +1007,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1217
1007
|
.option('--json', 'JSON output')
|
|
1218
1008
|
.action(async (id, opts) => {
|
|
1219
1009
|
try {
|
|
1220
|
-
const
|
|
1221
|
-
const tracked = getRoomRecord(id);
|
|
1222
|
-
if (tracked?.state === 'closing' || tracked?.state === 'closed')
|
|
1223
|
-
throw taskRoomPublicError('room_not_found', { room: id });
|
|
1224
|
-
const room = await coworkFor(cfg).getRoom(id);
|
|
1225
|
-
if (!room || room.state === 'closing' || room.state === 'closed')
|
|
1226
|
-
throw taskRoomPublicError('room_not_found', { room: id });
|
|
1010
|
+
const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1227
1011
|
const url = `http://localhost:4460/room/${id}`;
|
|
1228
1012
|
if (opts.json) {
|
|
1229
1013
|
console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
|
|
@@ -1249,15 +1033,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1249
1033
|
.option('--json', 'JSON output')
|
|
1250
1034
|
.action(async (id, opts) => {
|
|
1251
1035
|
try {
|
|
1252
|
-
const
|
|
1253
|
-
const r = getRoomRecord(id);
|
|
1254
|
-
if (r?.state === 'closing' || r?.state === 'closed')
|
|
1255
|
-
throw taskRoomPublicError('room_not_found', { room: id });
|
|
1256
|
-
const adapter = coworkFor(cfg);
|
|
1257
|
-
const room = await adapter.getRoom(id);
|
|
1258
|
-
if (!room || room.state === 'closing' || room.state === 'closed')
|
|
1259
|
-
throw taskRoomPublicError('room_not_found', { room: id });
|
|
1260
|
-
const members = await adapter.getSeats(id);
|
|
1036
|
+
const { orchestration: r, members } = await taskRoomService(opts.configuration).getRoomMembers(id);
|
|
1261
1037
|
if (opts.json) {
|
|
1262
1038
|
console.log(JSON.stringify({
|
|
1263
1039
|
schema_version: 1,
|
|
@@ -1291,11 +1067,9 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1291
1067
|
try {
|
|
1292
1068
|
if (id !== confirmId)
|
|
1293
1069
|
throw taskRoomPublicError('room_confirmation_mismatch');
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
throw taskRoomPublicError('room_record_not_found', { room: id });
|
|
1298
|
-
await acceptManagedRoomClose(id);
|
|
1070
|
+
await taskRoomService(opts.configuration).requestRoomDeletion({
|
|
1071
|
+
actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
|
|
1072
|
+
});
|
|
1299
1073
|
const settled = await launchRoomDeleteWorker(id, opts.configuration);
|
|
1300
1074
|
if (opts.json) {
|
|
1301
1075
|
console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
|
|
@@ -1333,10 +1107,10 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1333
1107
|
.option('--json', 'JSON output')
|
|
1334
1108
|
.action(async (id, opts) => {
|
|
1335
1109
|
try {
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (
|
|
1110
|
+
const recovered = await taskRoomService(opts.configuration).recoverRoom({
|
|
1111
|
+
actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
|
|
1112
|
+
});
|
|
1113
|
+
if (recovered.kind === 'deletion_worker_required') {
|
|
1340
1114
|
const settled = await launchRoomDeleteWorker(id, opts.configuration);
|
|
1341
1115
|
if (opts.json) {
|
|
1342
1116
|
console.log(JSON.stringify({
|
|
@@ -1351,72 +1125,13 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1351
1125
|
icon: settled.timedOut ? '⏳' : '🗑️',
|
|
1352
1126
|
title: settled.timedOut ? 'Room deletion pending' : 'Room deleted',
|
|
1353
1127
|
fields: [{ label: 'ID', value: id, kind: 'code' }],
|
|
1354
|
-
sections: [{ heading: 'Next step', items: [
|
|
1355
|
-
settled.timedOut
|
|
1128
|
+
sections: [{ heading: 'Next step', items: [settled.timedOut
|
|
1356
1129
|
? `Run ours-fleet room delete ${id} ${id} or room recover ${id} again.`
|
|
1357
|
-
: 'No recovery action is needed.',
|
|
1358
|
-
] }],
|
|
1130
|
+
: 'No recovery action is needed.'] }],
|
|
1359
1131
|
}));
|
|
1360
1132
|
return;
|
|
1361
1133
|
}
|
|
1362
|
-
const cowork =
|
|
1363
|
-
r = getRoomRecord(id);
|
|
1364
|
-
if (r && !r.owner_seat_cid
|
|
1365
|
-
&& (r.provisioning_detail === 'waiting_owner_invite'
|
|
1366
|
-
|| r.provisioning_detail === 'owner_cid_mismatch')) {
|
|
1367
|
-
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
1368
|
-
const existing = (await adapter.getSeats(id))
|
|
1369
|
-
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
1370
|
-
if (!existing && !cfg.ownerInvite)
|
|
1371
|
-
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
1372
|
-
let acceptedCid = existing?.identity_cid;
|
|
1373
|
-
if (!acceptedCid) {
|
|
1374
|
-
const accepted = await adapter.acceptInvite(id, cfg.ownerInvite, {
|
|
1375
|
-
role: cfg.rooms.owner.role,
|
|
1376
|
-
expected_cid: cfg.rooms.owner.expected_cid,
|
|
1377
|
-
});
|
|
1378
|
-
acceptedCid = accepted.seat_cid;
|
|
1379
|
-
}
|
|
1380
|
-
setOwnerSeat(id, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
1381
|
-
r = advanceSaga(id, 'create_members', 3);
|
|
1382
|
-
}
|
|
1383
|
-
const actions = [];
|
|
1384
|
-
if (r?.saga.error)
|
|
1385
|
-
actions.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
1386
|
-
if (r?.saga.recovery_hint)
|
|
1387
|
-
actions.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
|
|
1388
|
-
if (r?.provisioning_detail === 'waiting_cowork')
|
|
1389
|
-
actions.push('Check ours-cowork service status');
|
|
1390
|
-
if (r?.provisioning_detail === 'waiting_owner_invite')
|
|
1391
|
-
actions.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
1392
|
-
if (r?.provisioning_detail === 'waiting_seats')
|
|
1393
|
-
actions.push('Inspect temporary member logs for invite acceptance, then re-run recover');
|
|
1394
|
-
if (r && r.state === 'provisioning') {
|
|
1395
|
-
const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
|
|
1396
|
-
if (resumable.includes(r.saga.phase) && r.template_snapshot) {
|
|
1397
|
-
try {
|
|
1398
|
-
const template = r.task_id
|
|
1399
|
-
? durableTaskRoomTemplate(getTask(r.task_id), r)
|
|
1400
|
-
: r.template_snapshot;
|
|
1401
|
-
if (template) {
|
|
1402
|
-
await provisionMembers({
|
|
1403
|
-
cfg,
|
|
1404
|
-
cowork: adapter,
|
|
1405
|
-
roomId: r.room_id,
|
|
1406
|
-
taskId: r.task_id,
|
|
1407
|
-
template,
|
|
1408
|
-
binPath: getBinPath(),
|
|
1409
|
-
goal: r.room_name,
|
|
1410
|
-
});
|
|
1411
|
-
r = getRoomRecord(id);
|
|
1412
|
-
actions.splice(0, actions.length, 'Provisioning resumed successfully');
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
catch (error) {
|
|
1416
|
-
actions.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1134
|
+
const { room: cowork, orchestration: r, issues: actions } = recovered;
|
|
1420
1135
|
if (opts.json) {
|
|
1421
1136
|
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
|
|
1422
1137
|
return;
|
|
@@ -1428,8 +1143,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1428
1143
|
{ label: 'Status', value: roomStatus(cowork.state), kind: 'markdown' },
|
|
1429
1144
|
...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : []),
|
|
1430
1145
|
],
|
|
1431
|
-
sections: actions.length
|
|
1432
|
-
? [{ heading: 'Next steps', items: actions }]
|
|
1146
|
+
sections: actions.length ? [{ heading: 'Next steps', items: actions }]
|
|
1433
1147
|
: [{ heading: 'Result', items: ['No recovery action is needed.'] }],
|
|
1434
1148
|
}));
|
|
1435
1149
|
}
|
|
@@ -1441,8 +1155,10 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1441
1155
|
});
|
|
1442
1156
|
const internalDeleteAction = async (id, opts) => {
|
|
1443
1157
|
try {
|
|
1444
|
-
const
|
|
1445
|
-
const result = await
|
|
1158
|
+
const app = taskRoomService(opts.configuration);
|
|
1159
|
+
const result = await app.settleRoomDeletion({
|
|
1160
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, roomId: id,
|
|
1161
|
+
});
|
|
1446
1162
|
if (opts.json) {
|
|
1447
1163
|
console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
|
|
1448
1164
|
return;
|
|
@@ -1453,7 +1169,10 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1453
1169
|
}));
|
|
1454
1170
|
}
|
|
1455
1171
|
catch (e) {
|
|
1456
|
-
await
|
|
1172
|
+
await taskRoomService(opts.configuration).recordRoomSettlementError({
|
|
1173
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, roomId: id, error: errorText(e),
|
|
1174
|
+
recoveryHint: `External delete worker failed. Retry room delete ${id} ${id}.`,
|
|
1175
|
+
}).catch(() => { });
|
|
1457
1176
|
if (opts.json)
|
|
1458
1177
|
die(e);
|
|
1459
1178
|
dieTaskRoom(e);
|