@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.
- package/README.md +13 -9
- package/dist/application/errors.d.ts +1 -1
- package/dist/application/role-command-service.d.ts +29 -1
- package/dist/application/role-command-service.js +41 -2
- package/dist/application/role-creation-service.d.ts +32 -1
- package/dist/application/role-creation-service.js +56 -10
- package/dist/application/role-removal-service.d.ts +19 -0
- package/dist/application/role-removal-service.js +13 -3
- package/dist/application/session-mutations.d.ts +7 -0
- package/dist/application/session-mutations.js +8 -0
- package/dist/application/task-room-service.d.ts +262 -0
- package/dist/application/task-room-service.js +571 -0
- package/dist/atomic-file.d.ts +5 -3
- package/dist/atomic-file.js +32 -8
- package/dist/build-info.json +4 -4
- package/dist/cli.js +39 -15
- package/dist/config.d.ts +6 -3
- package/dist/config.js +0 -17
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +26 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/owner-channel/attachments.d.ts +2 -4
- package/dist/owner-channel/attachments.js +6 -39
- package/dist/owner-channel/channel.d.ts +5 -0
- package/dist/owner-channel/channel.js +159 -21
- package/dist/owner-channel/commands.d.ts +43 -1
- package/dist/owner-channel/commands.js +137 -130
- package/dist/rooms-tasks/cli.js +301 -463
- package/dist/rooms-tasks/task-lists.d.ts +19 -0
- package/dist/rooms-tasks/task-lists.js +96 -0
- package/dist/rooms-tasks/task-state.d.ts +3 -0
- package/dist/rooms-tasks/task-state.js +281 -175
- package/dist/rooms-tasks/types.d.ts +11 -1
- package/dist/runner.js +10 -33
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +22 -21
- package/dist/watchdog/query.d.ts +2 -0
- package/dist/watchdog/query.js +7 -3
- package/dist/web/runtime.js +2 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +88 -3
- package/package.json +1 -1
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -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,
|
|
8
|
-
import {
|
|
9
|
-
import { createRoomRecord, getRoomRecord,
|
|
10
|
-
import { createCoworkAdapter, CoworkProtocolError
|
|
11
|
-
import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from './types.js';
|
|
5
|
+
import { resolveTemplate, snapshotTemplate, } from './templates.js';
|
|
6
|
+
import { getTask, activateTask, TaskStateError, } from './task-state.js';
|
|
7
|
+
import { createRoomRecord, getRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
|
|
8
|
+
import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
|
|
12
9
|
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
|
|
10
|
+
import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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: ${
|
|
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
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
...(
|
|
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
|
|
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(
|
|
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
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const template = resolveRoomTemplate(cfg, t.template.name);
|
|
585
|
-
if (!template || template.content_hash !== t.template.content_hash)
|
|
586
|
-
throw taskRoomPublicError('task_template_drift', {
|
|
587
|
-
template: `${t.template.name}@${t.template.version}`,
|
|
588
|
-
});
|
|
589
|
-
await provisionRoom(cfg, {
|
|
590
|
-
name: t.title,
|
|
591
|
-
goal: t.title,
|
|
592
|
-
brief: t.brief,
|
|
593
|
-
template,
|
|
594
|
-
taskId: t.task_id,
|
|
595
|
-
onCreated: room => {
|
|
596
|
-
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
597
|
-
},
|
|
598
|
-
});
|
|
599
|
-
t = getTask(t.task_id);
|
|
600
|
-
}
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
throw new TaskStateError(`cannot transition from '${current.state}' to 'done'`);
|
|
684
|
-
let roomId;
|
|
685
|
-
let cfg;
|
|
686
|
-
if (current.room_id) {
|
|
687
|
-
cfg = loadCfg(opts);
|
|
688
|
-
const shouldClose = cfg.tasks?.close_room_on_done
|
|
689
|
-
?? cfg.rooms?.defaults?.close_when_task_done
|
|
690
|
-
?? false;
|
|
691
|
-
if (shouldClose)
|
|
692
|
-
roomId = current.room_id;
|
|
693
|
-
}
|
|
694
|
-
await acceptTaskTerminalIntent({ taskId: id, kind: 'done', roomId, outcome });
|
|
695
|
-
const settled = roomId
|
|
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:
|
|
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
|
|
727
|
-
|
|
728
|
-
throw new TaskStateError(`cannot cancel a '${current.state}' task`);
|
|
729
|
-
if (current.room_id)
|
|
730
|
-
loadCfg(opts);
|
|
731
|
-
await acceptTaskTerminalIntent({
|
|
732
|
-
taskId: id, kind: 'cancelled', roomId: current.room_id,
|
|
779
|
+
const plan = await taskRoomService(opts.configuration).cancelTask({
|
|
780
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
733
781
|
});
|
|
734
|
-
const settled =
|
|
782
|
+
const settled = plan.settlementRequired
|
|
735
783
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
736
|
-
: { task:
|
|
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(
|
|
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
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (resumable.includes(room.saga.phase)) {
|
|
816
|
-
const template = durableTaskRoomTemplate(t, room);
|
|
817
|
-
if (template) {
|
|
818
|
-
try {
|
|
819
|
-
await provisionMembers({
|
|
820
|
-
cfg,
|
|
821
|
-
cowork: coworkFor(cfg),
|
|
822
|
-
roomId: room.room_id,
|
|
823
|
-
taskId: t.task_id,
|
|
824
|
-
template,
|
|
825
|
-
binPath: getBinPath(),
|
|
826
|
-
brief: t.brief,
|
|
827
|
-
goal: t.title,
|
|
828
|
-
});
|
|
829
|
-
t = getTask(t.task_id);
|
|
830
|
-
room = getRoomRecord(room.room_id);
|
|
831
|
-
result.task = t;
|
|
832
|
-
result.room = room ?? null;
|
|
833
|
-
result.recovery_actions = ['Provisioning resumed successfully'];
|
|
834
|
-
}
|
|
835
|
-
catch (error) {
|
|
836
|
-
result.recovery_actions.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
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 mismatch — verify 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:
|
|
857
|
-
? [{ heading: 'Next steps', items:
|
|
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
|
|
873
|
-
const t = await
|
|
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
|
|
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
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (
|
|
898
|
-
// An explicit --template must agree with the room the task already
|
|
899
|
-
// runs in; a conflicting override is an error, not a silent no-op.
|
|
900
|
-
if (opts.template) {
|
|
901
|
-
const override = resolveTemplate(opts.template, allTemplates(cfg));
|
|
902
|
-
if (!override)
|
|
903
|
-
throw taskRoomPublicError('template_not_found', { template: opts.template });
|
|
904
|
-
const overrideSnap = snapshotTemplate(override);
|
|
905
|
-
const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
|
|
906
|
-
if (roomSnap && (roomSnap.name !== overrideSnap.name || roomSnap.content_hash !== overrideSnap.content_hash))
|
|
907
|
-
throw taskRoomPublicError('template_mismatch', {
|
|
908
|
-
requested: `${overrideSnap.name}@${overrideSnap.version}`, room: t.room_id,
|
|
909
|
-
provisioned: `${roomSnap.name}@${roomSnap.version}`,
|
|
910
|
-
});
|
|
911
|
-
}
|
|
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
|
-
...(
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
reviewTask(id);
|
|
1059
|
-
}
|
|
1060
|
-
if (t.room_id)
|
|
1061
|
-
loadCfg(opts); // validate configuration before durable acceptance
|
|
1062
|
-
await acceptTaskTerminalIntent({ taskId: id, kind: 'done', roomId: t.room_id, outcome });
|
|
1063
|
-
const settled = t.room_id
|
|
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:
|
|
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
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
brief = readFileSync(opts.briefFile, 'utf8');
|
|
1104
|
-
let templateSnapshot;
|
|
1105
|
-
if (opts.template) {
|
|
1106
|
-
const t = resolveTemplate(opts.template, allTemplates(cfg));
|
|
1107
|
-
if (!t)
|
|
1108
|
-
throw taskRoomPublicError('template_not_found', { template: opts.template });
|
|
1109
|
-
templateSnapshot = snapshotTemplate(t);
|
|
1110
|
-
}
|
|
1111
|
-
const record = await provisionRoom(cfg, {
|
|
1112
|
-
name: opts.name,
|
|
1113
|
-
goal: opts.goal,
|
|
1114
|
-
brief,
|
|
1115
|
-
template: templateSnapshot,
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (
|
|
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 =
|
|
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
|
|
1445
|
-
const result = await
|
|
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
|
|
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);
|