@ours.network/fleet 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +13 -9
  2. package/dist/application/errors.d.ts +1 -1
  3. package/dist/application/role-command-service.d.ts +29 -1
  4. package/dist/application/role-command-service.js +41 -2
  5. package/dist/application/role-creation-service.d.ts +32 -1
  6. package/dist/application/role-creation-service.js +56 -10
  7. package/dist/application/role-removal-service.d.ts +19 -0
  8. package/dist/application/role-removal-service.js +13 -3
  9. package/dist/application/session-mutations.d.ts +7 -0
  10. package/dist/application/session-mutations.js +8 -0
  11. package/dist/application/task-room-service.d.ts +262 -0
  12. package/dist/application/task-room-service.js +571 -0
  13. package/dist/atomic-file.d.ts +5 -3
  14. package/dist/atomic-file.js +32 -8
  15. package/dist/build-info.json +4 -4
  16. package/dist/cli.js +39 -15
  17. package/dist/config.d.ts +6 -3
  18. package/dist/config.js +0 -17
  19. package/dist/docs.d.ts +1 -1
  20. package/dist/docs.js +26 -4
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/owner-channel/attachments.d.ts +2 -4
  24. package/dist/owner-channel/attachments.js +6 -39
  25. package/dist/owner-channel/channel.d.ts +5 -0
  26. package/dist/owner-channel/channel.js +159 -21
  27. package/dist/owner-channel/commands.d.ts +43 -1
  28. package/dist/owner-channel/commands.js +137 -130
  29. package/dist/rooms-tasks/cli.js +301 -463
  30. package/dist/rooms-tasks/task-lists.d.ts +19 -0
  31. package/dist/rooms-tasks/task-lists.js +96 -0
  32. package/dist/rooms-tasks/task-state.d.ts +3 -0
  33. package/dist/rooms-tasks/task-state.js +281 -175
  34. package/dist/rooms-tasks/types.d.ts +11 -1
  35. package/dist/runner.js +10 -33
  36. package/dist/session/control.d.ts +10 -1
  37. package/dist/session/control.js +22 -21
  38. package/dist/watchdog/query.d.ts +2 -0
  39. package/dist/watchdog/query.js +7 -3
  40. package/dist/web/runtime.js +2 -0
  41. package/dist/web/server.d.ts +2 -0
  42. package/dist/web/server.js +88 -3
  43. package/package.json +1 -1
@@ -1,15 +1,14 @@
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';
11
+ import { TaskListError } from './task-lists.js';
13
12
  class TaskRoomPublicError extends Error {
14
13
  code;
15
14
  fields;
@@ -119,6 +118,8 @@ function isKnownRoomStateMessage(message) {
119
118
  ].some(pattern => pattern.test(message));
120
119
  }
121
120
  function dieTaskRoom(e) {
121
+ if (e instanceof TaskRoomApplicationError)
122
+ e = taskRoomPublicError(e.code, e.fields);
122
123
  if (e instanceof TaskRoomPublicError) {
123
124
  const failure = taskRoomPublicFailure(e);
124
125
  const output = renderMarkdownFailure({
@@ -128,6 +129,21 @@ function dieTaskRoom(e) {
128
129
  process.stderr.write(`${output}\n`);
129
130
  process.exit(1);
130
131
  }
132
+ if (e instanceof TaskListError) {
133
+ const notFound = e.code === 'list_not_found';
134
+ const conflict = ['duplicate_name', 'reserved_name', 'default_immutable',
135
+ 'destination_required', 'same_destination'].includes(e.code);
136
+ const output = renderMarkdownFailure({
137
+ kind: notFound ? 'not_found' : conflict ? 'state' : 'usage',
138
+ subject: 'ours-fleet task list',
139
+ detail: e.message,
140
+ action: notFound ? 'Run ours-fleet task lists to find a valid list.'
141
+ : conflict ? 'Choose a different list name or an explicit valid destination.'
142
+ : 'Use a valid NFC-normalized name of at most 64 Unicode code points.',
143
+ });
144
+ process.stderr.write(`${output}\n`);
145
+ process.exit(1);
146
+ }
131
147
  const taskMissing = e instanceof TaskStateError
132
148
  ? /^task not found: ([A-Za-z0-9_-]{1,128})$/u.exec(e.message) : null;
133
149
  const roomMissing = e instanceof RoomStateError
@@ -156,6 +172,7 @@ function dieTaskRoom(e) {
156
172
  const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
157
173
  icon: '📋', title, empty: 'No tasks found.',
158
174
  records: tasks.map(t => `${taskStatus(t.state)} ${markdownCode(t.task_id)} — ${markdownProse(t.title)}`
175
+ + ` — List ${markdownCode(t.list_name ?? 'default')}`
159
176
  + (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
160
177
  });
161
178
  const taskActionMarkdown = (title, task, fields = []) => renderMarkdownResult({
@@ -163,6 +180,7 @@ const taskActionMarkdown = (title, task, fields = []) => renderMarkdownResult({
163
180
  fields: [
164
181
  { label: 'ID', value: task.task_id, kind: 'code' },
165
182
  { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
183
+ { label: 'List', value: task.list_name ?? 'default', kind: 'code' },
166
184
  ...fields,
167
185
  ],
168
186
  });
@@ -186,7 +204,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
186
204
  await launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath);
187
205
  }
188
206
  catch (error) {
189
- await recordTaskTerminalIntentError(taskId, errorText(error), `External settle worker failed to start. Retry task recover ${taskId}.`);
207
+ await taskRoomService(configPath).recordSettlementError({
208
+ actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
209
+ recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
210
+ });
190
211
  throw error;
191
212
  }
192
213
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -207,7 +228,10 @@ async function launchRoomDeleteWorker(roomId, configPath) {
207
228
  await launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath);
208
229
  }
209
230
  catch (error) {
210
- await recordManagedRoomCloseError(roomId, errorText(error), `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`);
231
+ await taskRoomService(configPath).recordRoomSettlementError({
232
+ actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
233
+ recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
234
+ });
211
235
  throw error;
212
236
  }
213
237
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -225,6 +249,14 @@ async function launchRoomDeleteWorker(roomId, configPath) {
225
249
  function loadCfg(opts) {
226
250
  return loadConfig(opts.configuration);
227
251
  }
252
+ function taskRoomService(configuration) {
253
+ return new TaskRoomApplicationService(configuration, {
254
+ loadConfiguration: loadConfig,
255
+ cowork: coworkFor,
256
+ binPath: getBinPath,
257
+ provisionMembers,
258
+ });
259
+ }
228
260
  function allTemplates(cfg) {
229
261
  return cfg.roomTemplates ?? {};
230
262
  }
@@ -344,8 +376,7 @@ export function registerTemplateCommands(parent, cOpt) {
344
376
  .option('--json', 'JSON output')
345
377
  .action((opts) => {
346
378
  try {
347
- const cfg = loadCfg(opts);
348
- const templates = listTemplates(allTemplates(cfg));
379
+ const templates = taskRoomService(opts.configuration).listTemplates();
349
380
  if (opts.json) {
350
381
  console.log(JSON.stringify({ schema_version: 1, templates }, null, 2));
351
382
  return;
@@ -368,12 +399,9 @@ export function registerTemplateCommands(parent, cOpt) {
368
399
  .option('--json', 'JSON output')
369
400
  .action((name, opts) => {
370
401
  try {
371
- const cfg = loadCfg(opts);
372
- const t = resolveTemplate(name, allTemplates(cfg));
373
- if (!t)
374
- die(new Error(`template not found: ${name}`));
402
+ const t = taskRoomService(opts.configuration).getTemplate(name);
375
403
  if (opts.json) {
376
- console.log(JSON.stringify({ schema_version: 1, template: { ...t, content_hash: hashTemplate(t) } }, null, 2));
404
+ console.log(JSON.stringify({ schema_version: 1, template: t }, null, 2));
377
405
  return;
378
406
  }
379
407
  console.log(`${t.name}@${t.version} ${t.description}`);
@@ -382,7 +410,7 @@ export function registerTemplateCommands(parent, cOpt) {
382
410
  console.log(`\nMembers:`);
383
411
  for (const m of t.members)
384
412
  console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
385
- console.log(`\nContent hash: ${hashTemplate(t)}`);
413
+ console.log(`\nContent hash: ${t.content_hash}`);
386
414
  }
387
415
  catch (e) {
388
416
  die(e);
@@ -393,18 +421,7 @@ export function registerTemplateCommands(parent, cOpt) {
393
421
  .option('--json', 'JSON output')
394
422
  .action((opts) => {
395
423
  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
- }
424
+ const problems = taskRoomService(opts.configuration).validateTemplates();
408
425
  if (opts.json) {
409
426
  console.log(JSON.stringify({ schema_version: 1, valid: !problems.length, problems }, null, 2));
410
427
  return;
@@ -436,72 +453,29 @@ export function registerTaskCommands(parent, cOpt) {
436
453
  .option('--backlog', 'create in backlog (do not start immediately)')
437
454
  .option('--no-room', 'create task without a room')
438
455
  .option('--idempotency-key <key>', 'idempotency key')
456
+ .option('--list <name>', 'task list (default: default)')
439
457
  .option('--json', 'JSON output')
440
458
  .action(async (opts) => {
441
459
  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,
460
+ const record = await taskRoomService(opts.configuration).createTask({
461
+ actor: { kind: 'local_control', surface: 'cli' }, title: opts.title,
462
+ brief: opts.brief, briefFile: opts.briefFile, template: opts.template,
463
+ backlog: opts.backlog, noRoom: opts.room === false,
464
+ idempotencyKey: opts.idempotencyKey, origin: { type: 'cli' },
465
+ list: opts.list,
472
466
  });
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
467
  if (opts.json) {
496
468
  console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
497
469
  return;
498
470
  }
499
471
  console.log(taskActionMarkdown('Task created', record, [
500
472
  { label: 'Title', value: record.title },
501
- ...(templateRef ? [{ label: 'Template', value: `${templateRef.name}@${templateRef.version}`, kind: 'code' }] : []),
473
+ ...(record.template ? [{ label: 'Template', value: `${record.template.name}@${record.template.version}`, kind: 'code' }] : []),
502
474
  ]));
503
475
  }
504
476
  catch (e) {
477
+ if (e instanceof TaskRoomApplicationError && e.code === 'template_not_found')
478
+ e = taskRoomPublicError('template_not_found', { template: opts.template });
505
479
  if (opts.json)
506
480
  die(e);
507
481
  dieTaskRoom(e);
@@ -510,6 +484,8 @@ export function registerTaskCommands(parent, cOpt) {
510
484
  cOpt(taskCmd.command('list'))
511
485
  .description('list tasks')
512
486
  .option('--state <state>', 'filter by state (backlog|active|provisioning|review|done|cancelled|failed|all)')
487
+ .option('--list <name>', 'filter by task list')
488
+ .option('--group-by-list', 'group deterministic results by list (JSON)')
513
489
  .option('--json', 'JSON output')
514
490
  .action((opts) => {
515
491
  try {
@@ -517,9 +493,13 @@ export function registerTaskCommands(parent, cOpt) {
517
493
  if (opts.state && opts.state !== 'all') {
518
494
  stateFilter = opts.state;
519
495
  }
520
- const tasks = listTasks(stateFilter ? { state: stateFilter } : undefined);
496
+ const service = taskRoomService(opts.configuration);
497
+ const filter = { ...(stateFilter ? { state: stateFilter } : {}), ...(opts.list ? { list: opts.list } : {}) };
498
+ const tasks = service.listTasks(filter);
521
499
  if (opts.json) {
522
- console.log(JSON.stringify({ schema_version: 1, tasks }, null, 2));
500
+ console.log(JSON.stringify(opts.groupByList
501
+ ? { schema_version: 1, groups: service.groupedTasks(filter) }
502
+ : { schema_version: 1, tasks }, null, 2));
523
503
  return;
524
504
  }
525
505
  console.log(taskListMarkdown(tasks));
@@ -530,13 +510,105 @@ export function registerTaskCommands(parent, cOpt) {
530
510
  dieTaskRoom(e);
531
511
  }
532
512
  });
513
+ taskCmd.command('lists')
514
+ .description('list named task lists')
515
+ .option('--json', 'JSON output')
516
+ .action((opts) => {
517
+ try {
518
+ const lists = taskRoomService().listTaskLists();
519
+ if (opts.json) {
520
+ console.log(JSON.stringify({ schema_version: 1, lists }, null, 2));
521
+ return;
522
+ }
523
+ console.log(renderMarkdownList({ icon: '📚', title: 'Task lists', empty: 'No task lists found.',
524
+ records: lists.map(list => `${markdownCode(list.name)}${list.built_in ? ' — built-in' : ''}`) }));
525
+ }
526
+ catch (e) {
527
+ if (opts.json)
528
+ die(e);
529
+ dieTaskRoom(e);
530
+ }
531
+ });
532
+ taskCmd.command('list-create <name>')
533
+ .description('create a named task list')
534
+ .option('--json', 'JSON output')
535
+ .action(async (name, opts) => {
536
+ try {
537
+ const list = await taskRoomService().createTaskList({ actor: { kind: 'local_control', surface: 'cli' }, name });
538
+ if (opts.json) {
539
+ console.log(JSON.stringify({ schema_version: 1, list }, null, 2));
540
+ return;
541
+ }
542
+ console.log(renderMarkdownResult({ icon: '📚', title: 'Task list created', fields: [{ label: 'Name', value: list.name }] }));
543
+ }
544
+ catch (e) {
545
+ if (opts.json)
546
+ die(e);
547
+ dieTaskRoom(e);
548
+ }
549
+ });
550
+ taskCmd.command('list-rename <name> <new-name>')
551
+ .description('rename a named task list')
552
+ .option('--json', 'JSON output')
553
+ .action(async (name, newName, opts) => {
554
+ try {
555
+ const list = await taskRoomService().renameTaskList({ actor: { kind: 'local_control', surface: 'cli' }, name, newName });
556
+ if (opts.json) {
557
+ console.log(JSON.stringify({ schema_version: 1, list }, null, 2));
558
+ return;
559
+ }
560
+ console.log(renderMarkdownResult({ icon: '📚', title: 'Task list renamed', fields: [{ label: 'Name', value: list.name }] }));
561
+ }
562
+ catch (e) {
563
+ if (opts.json)
564
+ die(e);
565
+ dieTaskRoom(e);
566
+ }
567
+ });
568
+ taskCmd.command('list-delete <name>')
569
+ .description('delete a task list; non-empty lists require --move-to')
570
+ .option('--move-to <name>', 'destination for assigned tasks')
571
+ .option('--json', 'JSON output')
572
+ .action(async (name, opts) => {
573
+ try {
574
+ const result = await taskRoomService().deleteTaskList({ actor: { kind: 'local_control', surface: 'cli' }, name, destination: opts.moveTo });
575
+ if (opts.json) {
576
+ console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
577
+ return;
578
+ }
579
+ console.log(renderMarkdownResult({ icon: '🗑️', title: 'Task list deleted', fields: [{ label: 'Name', value: result.deleted.name }, { label: 'Tasks moved', value: String(result.moved) }] }));
580
+ }
581
+ catch (e) {
582
+ if (opts.json)
583
+ die(e);
584
+ dieTaskRoom(e);
585
+ }
586
+ });
587
+ taskCmd.command('move <id>')
588
+ .description('move a task to another list without changing its lifecycle')
589
+ .requiredOption('--list <name>', 'destination task list')
590
+ .option('--json', 'JSON output')
591
+ .action(async (id, opts) => {
592
+ try {
593
+ const task = await taskRoomService().moveTask({ actor: { kind: 'local_control', surface: 'cli' }, taskId: id, list: opts.list });
594
+ if (opts.json) {
595
+ console.log(JSON.stringify({ schema_version: 1, task }, null, 2));
596
+ return;
597
+ }
598
+ console.log(taskActionMarkdown('Task moved', task));
599
+ }
600
+ catch (e) {
601
+ if (opts.json)
602
+ die(e);
603
+ dieTaskRoom(e);
604
+ }
605
+ });
533
606
  taskCmd.command('show <id>')
534
607
  .description('show task details')
535
608
  .option('--json', 'JSON output')
536
609
  .action((id, opts) => {
537
610
  try {
538
- const t = getTask(id);
539
- const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
611
+ const { task: t, orchestration: room } = taskRoomService().getTask(id);
540
612
  if (opts.json) {
541
613
  console.log(JSON.stringify({
542
614
  schema_version: 1, task: t, orchestration: room ?? null,
@@ -578,26 +650,9 @@ export function registerTaskCommands(parent, cOpt) {
578
650
  .option('--json', 'JSON output')
579
651
  .action(async (id, opts) => {
580
652
  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
- }
653
+ const t = await taskRoomService(opts.configuration).startTask({
654
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
655
+ });
601
656
  if (opts.json) {
602
657
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
603
658
  return;
@@ -605,6 +660,11 @@ export function registerTaskCommands(parent, cOpt) {
605
660
  console.log(taskActionMarkdown('Task started', t));
606
661
  }
607
662
  catch (e) {
663
+ if (e instanceof TaskRoomApplicationError && e.code === 'task_template_drift')
664
+ e = taskRoomPublicError('task_template_drift', {
665
+ template: getTask(id).template
666
+ ? `${getTask(id).template.name}@${getTask(id).template.version}` : undefined,
667
+ });
608
668
  if (opts.json)
609
669
  die(e);
610
670
  dieTaskRoom(e);
@@ -616,7 +676,9 @@ export function registerTaskCommands(parent, cOpt) {
616
676
  .option('--json', 'JSON output')
617
677
  .action((id, opts) => {
618
678
  try {
619
- const t = blockTask(id, opts.reason);
679
+ const t = taskRoomService().blockTask({
680
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
681
+ });
620
682
  if (opts.json) {
621
683
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
622
684
  return;
@@ -634,7 +696,9 @@ export function registerTaskCommands(parent, cOpt) {
634
696
  .option('--json', 'JSON output')
635
697
  .action((id, opts) => {
636
698
  try {
637
- const t = unblockTask(id);
699
+ const t = taskRoomService().unblockTask({
700
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
701
+ });
638
702
  if (opts.json) {
639
703
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
640
704
  return;
@@ -652,7 +716,9 @@ export function registerTaskCommands(parent, cOpt) {
652
716
  .option('--json', 'JSON output')
653
717
  .action((id, opts) => {
654
718
  try {
655
- const t = reviewTask(id);
719
+ const t = taskRoomService().reviewTask({
720
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
721
+ });
656
722
  if (opts.json) {
657
723
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
658
724
  return;
@@ -676,25 +742,12 @@ export function registerTaskCommands(parent, cOpt) {
676
742
  if (opts.summaryFile)
677
743
  summary = readFileSync(opts.summaryFile, 'utf8');
678
744
  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
745
+ const plan = await taskRoomService(opts.configuration).completeTask({
746
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
747
+ });
748
+ const settled = plan.settlementRequired
696
749
  ? await launchTaskSettleWorker(id, opts.configuration)
697
- : { task: getTask(id), timedOut: false };
750
+ : { task: plan.task, timedOut: false };
698
751
  const t = settled.task;
699
752
  if (opts.json) {
700
753
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
@@ -723,17 +776,12 @@ export function registerTaskCommands(parent, cOpt) {
723
776
  try {
724
777
  if (id !== confirmId)
725
778
  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,
779
+ const plan = await taskRoomService(opts.configuration).cancelTask({
780
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
733
781
  });
734
- const settled = current.room_id
782
+ const settled = plan.settlementRequired
735
783
  ? await launchTaskSettleWorker(id, opts.configuration)
736
- : { task: getTask(id), timedOut: false };
784
+ : { task: plan.task, timedOut: false };
737
785
  const t = settled.task;
738
786
  if (opts.json) {
739
787
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
@@ -762,7 +810,9 @@ export function registerTaskCommands(parent, cOpt) {
762
810
  try {
763
811
  if (id !== confirmId)
764
812
  throw taskRoomPublicError('task_confirmation_mismatch');
765
- const deleted = deleteTask(id);
813
+ const deleted = taskRoomService().deleteTask({
814
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
815
+ });
766
816
  if (opts.json) {
767
817
  console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
768
818
  return;
@@ -783,61 +833,35 @@ export function registerTaskCommands(parent, cOpt) {
783
833
  .option('--json', 'JSON output')
784
834
  .action(async (id, opts) => {
785
835
  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
- }
836
+ const app = taskRoomService(opts.configuration);
837
+ const actor = { kind: 'local_control', surface: 'cli' };
838
+ const begin = await app.beginTaskRecovery({ actor, taskId: id });
839
+ const recovered = begin.kind === 'terminal_worker_required'
840
+ ? await (async () => {
841
+ const settled = await launchTaskSettleWorker(id, opts.configuration);
842
+ return app.continueTaskRecovery({ actor, taskId: id, terminalTimedOut: settled.timedOut });
843
+ })()
844
+ : begin.result;
845
+ const t = recovered.task;
846
+ const room = recovered.room;
847
+ const recoveryActions = recovered.issues.map(issue => {
848
+ if (issue.code === 'terminal_pending')
849
+ return `Terminal intent remains pending — retry task recover ${id}`;
850
+ if (issue.code === 'waiting_cowork')
851
+ return 'Cowork management socket unreachable check ours-cowork service';
852
+ if (issue.code === 'waiting_owner_invite')
853
+ return 'Owner invite invalid or expired — rotate rooms.owner.public_invite in config';
854
+ if (issue.code === 'owner_cid_mismatch')
855
+ return 'Owner CID mismatchverify rooms.owner.expected_cid matches Messenger identity';
856
+ if (issue.code === 'member_failed')
857
+ return `Member creation failed at saga step ${issue.stepIndex} inspect and retry`;
858
+ if (issue.code === 'waiting_seats')
859
+ return 'Members have not accepted their one-time room invites yet inspect role logs, then retry recovery';
860
+ if (issue.code === 'resume_failed')
861
+ return `Resume failed: ${issue.error}`;
862
+ return 'Provisioning resumed successfully';
863
+ });
864
+ const result = { task: t, room: room ?? null, recovery_actions: recoveryActions };
841
865
  if (opts.json) {
842
866
  console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
843
867
  return;
@@ -853,8 +877,8 @@ export function registerTaskCommands(parent, cOpt) {
853
877
  { label: 'Saga', value: room.saga.phase, kind: 'code' },
854
878
  ] : []),
855
879
  ],
856
- sections: result.recovery_actions.length
857
- ? [{ heading: 'Next steps', items: result.recovery_actions }]
880
+ sections: recoveryActions.length
881
+ ? [{ heading: 'Next steps', items: recoveryActions }]
858
882
  : [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
859
883
  }));
860
884
  }
@@ -869,8 +893,10 @@ export function registerTaskCommands(parent, cOpt) {
869
893
  .option('--json', 'JSON output')
870
894
  .action(async (id, opts) => {
871
895
  try {
872
- const cfg = loadCfg(opts);
873
- const t = await settleTaskTerminalIntent({ taskId: id, cowork: coworkFor(cfg) });
896
+ const app = taskRoomService(opts.configuration);
897
+ const t = await app.settleTask({
898
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
899
+ });
874
900
  if (opts.json) {
875
901
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
876
902
  return;
@@ -878,7 +904,41 @@ export function registerTaskCommands(parent, cOpt) {
878
904
  console.log(taskActionMarkdown('Task terminal action settled', t));
879
905
  }
880
906
  catch (e) {
881
- await recordTaskTerminalIntentError(id, errorText(e), `External settle worker failed. Retry task recover ${id}.`).catch(() => { });
907
+ await taskRoomService(opts.configuration).recordSettlementError({
908
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
909
+ error: errorText(e), recoveryHint: `External settle worker failed. Retry task recover ${id}.`,
910
+ }).catch(() => { });
911
+ if (opts.json)
912
+ die(e);
913
+ dieTaskRoom(e);
914
+ }
915
+ });
916
+ cOpt(taskCmd.command('_recover <id>', { hidden: true }))
917
+ .description('internal: settle and continue task recovery')
918
+ .option('--json', 'JSON output')
919
+ .action(async (id, opts) => {
920
+ const app = taskRoomService(opts.configuration);
921
+ const actor = { kind: 'internal_worker', surface: 'cli' };
922
+ try {
923
+ const begin = await app.beginTaskRecovery({ actor, taskId: id });
924
+ const result = begin.kind === 'terminal_worker_required'
925
+ ? await (async () => {
926
+ try {
927
+ await app.settleTask({ actor, taskId: id });
928
+ }
929
+ catch (error) {
930
+ await app.recordSettlementError({
931
+ actor, taskId: id, error: errorText(error),
932
+ recoveryHint: `External settle worker failed. Retry task recover ${id}.`,
933
+ }).catch(() => { });
934
+ throw error;
935
+ }
936
+ return app.continueTaskRecovery({ actor, taskId: id, terminalTimedOut: false });
937
+ })()
938
+ : begin.result;
939
+ console.log(JSON.stringify({ schema_version: 1, recovery: result }, null, 2));
940
+ }
941
+ catch (e) {
882
942
  if (opts.json)
883
943
  die(e);
884
944
  dieTaskRoom(e);
@@ -890,25 +950,11 @@ export function registerTaskCommands(parent, cOpt) {
890
950
  .option('--json', 'JSON output')
891
951
  .action(async (id, opts) => {
892
952
  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
- }
953
+ const result = await taskRoomService(opts.configuration).ensureTaskWork({
954
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id, template: opts.template,
955
+ });
956
+ const t = result.task;
957
+ if (result.status === 'already_active') {
912
958
  if (opts.json) {
913
959
  console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
914
960
  return;
@@ -916,107 +962,6 @@ export function registerTaskCommands(parent, cOpt) {
916
962
  console.log(taskActionMarkdown('Task already active', t, t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []));
917
963
  return;
918
964
  }
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
965
  if (opts.json) {
1021
966
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
1022
967
  return;
@@ -1026,15 +971,15 @@ export function registerTaskCommands(parent, cOpt) {
1026
971
  fields: [
1027
972
  { label: 'ID', value: t.task_id, kind: 'code' },
1028
973
  { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
1029
- ...(templateRef ? [{ label: 'Template', value: `${templateRef.name}@${templateRef.version}`, kind: 'code' }] : []),
974
+ ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
1030
975
  ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
1031
976
  ],
1032
- sections: t.member_roles.length ? [{
1033
- heading: 'Agents', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)}`),
1034
- }] : [],
977
+ sections: t.member_roles.length ? [{ heading: 'Agents', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)}`) }] : [],
1035
978
  }));
1036
979
  }
1037
980
  catch (e) {
981
+ if (e instanceof TaskRoomApplicationError)
982
+ e = taskRoomPublicError(e.code, e.fields);
1038
983
  if (opts.json)
1039
984
  die(e);
1040
985
  dieTaskRoom(e);
@@ -1051,19 +996,13 @@ export function registerTaskCommands(parent, cOpt) {
1051
996
  if (opts.summaryFile)
1052
997
  summary = readFileSync(opts.summaryFile, 'utf8');
1053
998
  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
999
+ const plan = await taskRoomService(opts.configuration).finishTask({
1000
+ actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
1001
+ });
1002
+ const settled = plan.settlementRequired
1064
1003
  ? await launchTaskSettleWorker(id, opts.configuration)
1065
- : { task: getTask(id), timedOut: false };
1066
- t = settled.task;
1004
+ : { task: plan.task, timedOut: false };
1005
+ const t = settled.task;
1067
1006
  if (opts.json) {
1068
1007
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
1069
1008
  return;
@@ -1079,6 +1018,8 @@ export function registerTaskCommands(parent, cOpt) {
1079
1018
  console.log(taskActionMarkdown('Task finished', t, t.outcome ? [{ label: 'Summary', value: t.outcome.summary, multiline: true }] : []));
1080
1019
  }
1081
1020
  catch (e) {
1021
+ if (e instanceof TaskRoomApplicationError)
1022
+ e = taskRoomPublicError(e.code, e.fields);
1082
1023
  if (opts.json)
1083
1024
  die(e);
1084
1025
  dieTaskRoom(e);
@@ -1097,22 +1038,9 @@ export function registerRoomCommands(parent, cOpt) {
1097
1038
  .option('--json', 'JSON output')
1098
1039
  .action(async (opts) => {
1099
1040
  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,
1041
+ const record = await taskRoomService(opts.configuration).createRoom({
1042
+ actor: { kind: 'local_control', surface: 'cli' }, name: opts.name,
1043
+ template: opts.template, goal: opts.goal, brief: opts.brief, briefFile: opts.briefFile,
1116
1044
  });
1117
1045
  if (opts.json) {
1118
1046
  console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
@@ -1120,7 +1048,7 @@ export function registerRoomCommands(parent, cOpt) {
1120
1048
  }
1121
1049
  console.log(roomActionMarkdown('Room created', record, [
1122
1050
  { label: 'Name', value: record.room_name },
1123
- ...(templateSnapshot ? [{ label: 'Template', value: `${templateSnapshot.name}@${templateSnapshot.version}`, kind: 'code' }] : []),
1051
+ ...(record.template_snapshot ? [{ label: 'Template', value: `${record.template_snapshot.name}@${record.template_snapshot.version}`, kind: 'code' }] : []),
1124
1052
  ]));
1125
1053
  }
1126
1054
  catch (e) {
@@ -1135,23 +1063,11 @@ export function registerRoomCommands(parent, cOpt) {
1135
1063
  .option('--json', 'JSON output')
1136
1064
  .action(async (opts) => {
1137
1065
  try {
1138
- const cfg = loadCfg(opts);
1139
1066
  if (opts.state && !['active', 'provisioning', 'all'].includes(opts.state))
1140
1067
  throw taskRoomPublicError('room_filter');
1141
1068
  const stateFilter = opts.state === 'active' || opts.state === 'provisioning'
1142
1069
  ? 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 }));
1070
+ const rooms = await taskRoomService(opts.configuration).listRooms(stateFilter ? { state: stateFilter } : undefined);
1155
1071
  if (opts.json) {
1156
1072
  console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
1157
1073
  return;
@@ -1173,14 +1089,7 @@ export function registerRoomCommands(parent, cOpt) {
1173
1089
  .option('--json', 'JSON output')
1174
1090
  .action(async (id, opts) => {
1175
1091
  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;
1092
+ const { room: cowork, orchestration: r } = await taskRoomService(opts.configuration).getRoomDetail(id);
1184
1093
  if (opts.json) {
1185
1094
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
1186
1095
  return;
@@ -1217,13 +1126,7 @@ export function registerRoomCommands(parent, cOpt) {
1217
1126
  .option('--json', 'JSON output')
1218
1127
  .action(async (id, opts) => {
1219
1128
  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 });
1129
+ const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
1227
1130
  const url = `http://localhost:4460/room/${id}`;
1228
1131
  if (opts.json) {
1229
1132
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
@@ -1249,15 +1152,7 @@ export function registerRoomCommands(parent, cOpt) {
1249
1152
  .option('--json', 'JSON output')
1250
1153
  .action(async (id, opts) => {
1251
1154
  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);
1155
+ const { orchestration: r, members } = await taskRoomService(opts.configuration).getRoomMembers(id);
1261
1156
  if (opts.json) {
1262
1157
  console.log(JSON.stringify({
1263
1158
  schema_version: 1,
@@ -1291,11 +1186,9 @@ export function registerRoomCommands(parent, cOpt) {
1291
1186
  try {
1292
1187
  if (id !== confirmId)
1293
1188
  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);
1189
+ await taskRoomService(opts.configuration).requestRoomDeletion({
1190
+ actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1191
+ });
1299
1192
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1300
1193
  if (opts.json) {
1301
1194
  console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
@@ -1333,10 +1226,10 @@ export function registerRoomCommands(parent, cOpt) {
1333
1226
  .option('--json', 'JSON output')
1334
1227
  .action(async (id, opts) => {
1335
1228
  try {
1336
- const cfg = loadCfg(opts);
1337
- const adapter = coworkFor(cfg);
1338
- let r = getRoomRecord(id);
1339
- if (r?.state === 'closing' || r?.state === 'closed') {
1229
+ const recovered = await taskRoomService(opts.configuration).recoverRoom({
1230
+ actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1231
+ });
1232
+ if (recovered.kind === 'deletion_worker_required') {
1340
1233
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1341
1234
  if (opts.json) {
1342
1235
  console.log(JSON.stringify({
@@ -1351,72 +1244,13 @@ export function registerRoomCommands(parent, cOpt) {
1351
1244
  icon: settled.timedOut ? '⏳' : '🗑️',
1352
1245
  title: settled.timedOut ? 'Room deletion pending' : 'Room deleted',
1353
1246
  fields: [{ label: 'ID', value: id, kind: 'code' }],
1354
- sections: [{ heading: 'Next step', items: [
1355
- settled.timedOut
1247
+ sections: [{ heading: 'Next step', items: [settled.timedOut
1356
1248
  ? `Run ours-fleet room delete ${id} ${id} or room recover ${id} again.`
1357
- : 'No recovery action is needed.',
1358
- ] }],
1249
+ : 'No recovery action is needed.'] }],
1359
1250
  }));
1360
1251
  return;
1361
1252
  }
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
- }
1253
+ const { room: cowork, orchestration: r, issues: actions } = recovered;
1420
1254
  if (opts.json) {
1421
1255
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
1422
1256
  return;
@@ -1428,8 +1262,7 @@ export function registerRoomCommands(parent, cOpt) {
1428
1262
  { label: 'Status', value: roomStatus(cowork.state), kind: 'markdown' },
1429
1263
  ...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : []),
1430
1264
  ],
1431
- sections: actions.length
1432
- ? [{ heading: 'Next steps', items: actions }]
1265
+ sections: actions.length ? [{ heading: 'Next steps', items: actions }]
1433
1266
  : [{ heading: 'Result', items: ['No recovery action is needed.'] }],
1434
1267
  }));
1435
1268
  }
@@ -1441,8 +1274,10 @@ export function registerRoomCommands(parent, cOpt) {
1441
1274
  });
1442
1275
  const internalDeleteAction = async (id, opts) => {
1443
1276
  try {
1444
- const cfg = loadCfg(opts);
1445
- const result = await deleteManagedRoom({ roomId: id, cowork: coworkFor(cfg) });
1277
+ const app = taskRoomService(opts.configuration);
1278
+ const result = await app.settleRoomDeletion({
1279
+ actor: { kind: 'internal_worker', surface: 'cli' }, roomId: id,
1280
+ });
1446
1281
  if (opts.json) {
1447
1282
  console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
1448
1283
  return;
@@ -1453,7 +1288,10 @@ export function registerRoomCommands(parent, cOpt) {
1453
1288
  }));
1454
1289
  }
1455
1290
  catch (e) {
1456
- await recordManagedRoomCloseError(id, errorText(e), `External delete worker failed. Retry room delete ${id} ${id}.`).catch(() => { });
1291
+ await taskRoomService(opts.configuration).recordRoomSettlementError({
1292
+ actor: { kind: 'internal_worker', surface: 'cli' }, roomId: id, error: errorText(e),
1293
+ recoveryHint: `External delete worker failed. Retry room delete ${id} ${id}.`,
1294
+ }).catch(() => { });
1457
1295
  if (opts.json)
1458
1296
  die(e);
1459
1297
  dieTaskRoom(e);