@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.
@@ -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, listTemplates, snapshotTemplate, hashTemplate, } from './templates.js';
8
- import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unblockTask, reviewTask, updateTaskRoom, updateTaskTemplate, deleteTask, TaskStateError, } from './task-state.js';
9
- import { createRoomRecord, getRoomRecord, listRoomRecords, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
10
- import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
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 recordTaskTerminalIntentError(taskId, errorText(error), `External settle worker failed to start. Retry task recover ${taskId}.`);
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 recordManagedRoomCloseError(roomId, errorText(error), `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`);
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 cfg = loadCfg(opts);
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 cfg = loadCfg(opts);
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: { ...t, content_hash: hashTemplate(t) } }, null, 2));
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: ${hashTemplate(t)}`);
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 cfg = loadCfg(opts);
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 cfg = loadCfg(opts);
443
- let templateRef;
444
- if (opts.template) {
445
- const t = resolveTemplate(opts.template, allTemplates(cfg));
446
- if (!t)
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
- ...(templateRef ? [{ label: 'Template', value: `${templateRef.name}@${templateRef.version}`, kind: 'code' }] : []),
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 cfg = loadCfg(opts);
582
- let t = startTask(id);
583
- if (t.template && !t.room_id) {
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(id, opts.reason);
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(id);
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(id);
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 current = getTask(id);
680
- if (TASK_TERMINAL_STATES.includes(current.state))
681
- throw new TaskStateError(`task ${id} is already in terminal state '${current.state}'`);
682
- if (current.state !== 'review')
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: getTask(id), timedOut: false };
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 current = getTask(id);
727
- if (!TASK_CANCELLABLE_STATES.includes(current.state))
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 = current.room_id
663
+ const settled = plan.settlementRequired
735
664
  ? await launchTaskSettleWorker(id, opts.configuration)
736
- : { task: getTask(id), timedOut: false };
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(id);
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 cfg = loadCfg(opts);
787
- let t = getTask(id);
788
- let terminalTimedOut = false;
789
- if (t.terminal_intent?.status === 'pending') {
790
- const settled = await launchTaskSettleWorker(id, opts.configuration);
791
- t = settled.task;
792
- terminalTimedOut = settled.timedOut;
793
- }
794
- let room = t.room_id ? getRoomRecord(t.room_id) : undefined;
795
- const result = {
796
- task: t,
797
- room: room ?? null,
798
- recovery_actions: [],
799
- };
800
- if (terminalTimedOut) {
801
- result.recovery_actions.push(`Terminal intent remains pendingretry task recover ${id}`);
802
- }
803
- if (t.state === 'provisioning' && room) {
804
- if (room.provisioning_detail === 'waiting_cowork')
805
- result.recovery_actions.push('Cowork management socket unreachable check ours-cowork service');
806
- if (room.provisioning_detail === 'waiting_owner_invite')
807
- result.recovery_actions.push('Owner invite invalid or expired rotate rooms.owner.public_invite in config');
808
- if (room.provisioning_detail === 'owner_cid_mismatch')
809
- result.recovery_actions.push('Owner CID mismatchverify rooms.owner.expected_cid matches Messenger identity');
810
- if (room.provisioning_detail === 'member_failed')
811
- result.recovery_actions.push(`Member creation failed at saga step ${room.saga.step_index} — inspect and retry`);
812
- if (room.provisioning_detail === 'waiting_seats')
813
- result.recovery_actions.push('Members have not accepted their one-time room invites yet — inspect role logs, then retry recovery');
814
- const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
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 mismatchverify 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: result.recovery_actions.length
857
- ? [{ heading: 'Next steps', items: result.recovery_actions }]
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 cfg = loadCfg(opts);
873
- const t = await settleTaskTerminalIntent({ taskId: id, cowork: coworkFor(cfg) });
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 recordTaskTerminalIntentError(id, errorText(e), `External settle worker failed. Retry task recover ${id}.`).catch(() => { });
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 cfg = loadCfg(opts);
894
- let t = getTask(id);
895
- if (TASK_TERMINAL_STATES.includes(t.state))
896
- throw taskRoomPublicError('task_terminal', { task: id, state: t.state });
897
- if (t.state === 'active' && t.room_id) {
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
- ...(templateRef ? [{ label: 'Template', value: `${templateRef.name}@${templateRef.version}`, kind: 'code' }] : []),
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
- let t = getTask(id);
1055
- if (TASK_TERMINAL_STATES.includes(t.state))
1056
- throw taskRoomPublicError('task_terminal_already', { task: id, state: t.state });
1057
- if (t.state === 'active') {
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: getTask(id), timedOut: false };
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 cfg = loadCfg(opts);
1101
- let brief = opts.brief;
1102
- if (opts.briefFile)
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
- ...(templateSnapshot ? [{ label: 'Template', value: `${templateSnapshot.name}@${templateSnapshot.version}`, kind: 'code' }] : []),
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 adapter = coworkFor(cfg);
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 cfg = loadCfg(opts);
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 cfg = loadCfg(opts);
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 cfg = loadCfg(opts);
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
- coworkFor(loadCfg(opts));
1295
- const existing = getRoomRecord(id);
1296
- if (!existing)
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 cfg = loadCfg(opts);
1337
- const adapter = coworkFor(cfg);
1338
- let r = getRoomRecord(id);
1339
- if (r?.state === 'closing' || r?.state === 'closed') {
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 = await adapter.recoverRoom(id);
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 cfg = loadCfg(opts);
1445
- const result = await deleteManagedRoom({ roomId: id, cowork: coworkFor(cfg) });
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 recordManagedRoomCloseError(id, errorText(e), `External delete worker failed. Retry room delete ${id} ${id}.`).catch(() => { });
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);