@ours.network/fleet 2.2.5 → 2.3.0

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 (38) hide show
  1. package/dist/application/task-room-service.d.ts +11 -0
  2. package/dist/application/task-room-service.js +37 -14
  3. package/dist/atomic-file.d.ts +2 -0
  4. package/dist/atomic-file.js +2 -1
  5. package/dist/build-info.json +4 -4
  6. package/dist/config.d.ts +6 -0
  7. package/dist/config.js +6 -3
  8. package/dist/docs.d.ts +1 -1
  9. package/dist/docs.js +37 -0
  10. package/dist/owner-channel/commands.js +25 -12
  11. package/dist/reports/tasks.js +4 -3
  12. package/dist/rooms-tasks/add-member.js +17 -6
  13. package/dist/rooms-tasks/cli.js +79 -5
  14. package/dist/rooms-tasks/config.js +11 -0
  15. package/dist/rooms-tasks/layout-member-retirement.d.ts +15 -0
  16. package/dist/rooms-tasks/layout-member-retirement.js +26 -1
  17. package/dist/rooms-tasks/layout-service.d.ts +2 -1
  18. package/dist/rooms-tasks/layout-service.js +10 -4
  19. package/dist/rooms-tasks/layout.d.ts +4 -2
  20. package/dist/rooms-tasks/layout.js +21 -4
  21. package/dist/rooms-tasks/member-naming.d.ts +43 -0
  22. package/dist/rooms-tasks/member-naming.js +186 -0
  23. package/dist/rooms-tasks/member-overrides.js +1 -0
  24. package/dist/rooms-tasks/mission-keys.d.ts +83 -0
  25. package/dist/rooms-tasks/mission-keys.js +250 -0
  26. package/dist/rooms-tasks/provision.d.ts +4 -0
  27. package/dist/rooms-tasks/provision.js +37 -3
  28. package/dist/rooms-tasks/task-lists.d.ts +18 -2
  29. package/dist/rooms-tasks/task-lists.js +60 -5
  30. package/dist/rooms-tasks/task-room-name.d.ts +7 -2
  31. package/dist/rooms-tasks/task-room-name.js +13 -4
  32. package/dist/rooms-tasks/task-state.d.ts +40 -0
  33. package/dist/rooms-tasks/task-state.js +288 -45
  34. package/dist/rooms-tasks/types.d.ts +35 -1
  35. package/dist/rooms-tasks/types.js +1 -1
  36. package/dist/web/runtime.js +4 -1
  37. package/dist/web/server.js +62 -27
  38. package/package.json +5 -5
@@ -174,6 +174,15 @@ export interface TaskRecord {
174
174
  notification_manifest?: import('./notification-manifest.js').TaskNotificationManifest;
175
175
  workspace?: import('./workspace.js').OwnedWorkspace;
176
176
  task_id: string;
177
+ /**
178
+ * Immutable human mission key (`MSN-1`), issued once from a durable
179
+ * per-prefix high-water mark. Absent only before the one-time migration.
180
+ */
181
+ key?: string;
182
+ /** Set when the key was backfilled for a task that existed before mission keys. */
183
+ key_from_migration?: true;
184
+ /** Durable per-mission agent name allocations; absent until first allocation or migration. */
185
+ member_naming?: MemberNaming;
177
186
  /** Stable organizational list identifier. Missing legacy values mean `default`. */
178
187
  list_id: string;
179
188
  /** Derived presentation field; repositories must never persist it. */
@@ -211,9 +220,30 @@ export interface TaskRecord {
211
220
  terminal_intent?: TaskTerminalIntent;
212
221
  deletion?: TaskDeletionIntent;
213
222
  }
223
+ /**
224
+ * Role-scoped agent names: `<KEY>-<Stem>` for the first member of a stem,
225
+ * `<KEY>-<Stem>-<n>` (n >= 2) after it. Counters only grow; an allocation is
226
+ * reused by provisioning, recovery and restart, never by a replacement.
227
+ */
228
+ export interface MemberNameAllocation {
229
+ name: string;
230
+ /** Counter bucket: the case-folded truncated stem. Absent on frozen legacy names. */
231
+ bucket?: string;
232
+ ordinal?: number;
233
+ label_source?: 'role_override' | 'template_ref' | 'room_role' | 'participant_key' | 'slot' | 'fallback';
234
+ /** A historical name kept exactly as launched before mission keys existed. */
235
+ frozen?: true;
236
+ }
237
+ export interface MemberNaming {
238
+ version: 2;
239
+ counters: Record<string, number>;
240
+ allocations: Record<string, MemberNameAllocation>;
241
+ }
214
242
  export interface TaskListRecord {
215
243
  list_id: string;
216
244
  name: string;
245
+ /** Prefix for new mission keys; absent only before the one-time key migration. */
246
+ prefix?: string;
217
247
  built_in: boolean;
218
248
  created_at: string;
219
249
  }
@@ -387,6 +417,8 @@ export interface TemplateSnapshotMember extends TemplateMemberSlot {
387
417
  id: string;
388
418
  hash: string;
389
419
  };
420
+ /** Named Role of the member's Agent Template, sealed at plan time for agent naming. */
421
+ role_label?: string;
390
422
  }
391
423
  export interface TemplateSnapshot extends Omit<TemplateDefinition, 'members'> {
392
424
  members: TemplateSnapshotMember[];
@@ -418,13 +450,15 @@ export interface TasksConfig {
418
450
  create_mode?: 'start' | 'backlog';
419
451
  close_room_on_done?: boolean;
420
452
  retain_completed_for?: string;
453
+ /** Default list prefix (default MSN); may change only before the first mission key. */
454
+ default_prefix?: string;
421
455
  }
422
456
  export type RoomTemplatesConfig = Record<string, TemplateDefinition>;
423
457
  export declare const ROOMS_KEYS: readonly ["cowork", "owner", "defaults"];
424
458
  export declare const ROOMS_OWNER_KEYS: readonly ["provider", "public_invite", "public_invite_file", "expected_cid", "role"];
425
459
  export declare const ROOMS_COWORK_KEYS: readonly ["config"];
426
460
  export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
427
- export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
461
+ export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for", "default_prefix"];
428
462
  export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
429
463
  export declare const TEMPLATE_ROOM_KEYS: readonly ["quiet_membership", "anonymous"];
430
464
  /** `agent` is accepted only so validation can emit its actionable migration error. */
@@ -23,7 +23,7 @@ export const ROOMS_KEYS = ['cowork', 'owner', 'defaults'];
23
23
  export const ROOMS_OWNER_KEYS = ['provider', 'public_invite', 'public_invite_file', 'expected_cid', 'role'];
24
24
  export const ROOMS_COWORK_KEYS = ['config'];
25
25
  export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
26
- export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
26
+ export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for', 'default_prefix'];
27
27
  export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
28
28
  export const TEMPLATE_ROOM_KEYS = ['quiet_membership', 'anonymous'];
29
29
  /** `agent` is accepted only so validation can emit its actionable migration error. */
@@ -193,13 +193,16 @@ export async function startWebConsole(options) {
193
193
  const topologyPromote = new TopologyPromoteService({
194
194
  drafts: topologyDrafts, configuration, topology: readTopology,
195
195
  });
196
+ const taskRooms = new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath });
197
+ // One-time, lock-guarded mission key migration before serving; failure leaves it to the next mutation.
198
+ await taskRooms.prepareMissionKeys().catch(error => console.warn(`mission key migration deferred: ${error.message}`));
196
199
  let server;
197
200
  try {
198
201
  server = await buildWebServer({
199
202
  query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration, observeRole,
200
203
  resourceScope: { configPath: configuration.path, subscribeMutations: listener => events.subscribeResources(listener) },
201
204
  invalidateResourceCaches: () => { watchdogConfigProvider.invalidate(); watchdogFindings.invalidate(); },
202
- taskRooms: new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath }),
205
+ taskRooms,
203
206
  roomLayouts: new RoomLayoutDefinitions(options.configPath),
204
207
  layoutBindings: new LayoutBindingGrants(new RoomLayoutService(options.configPath).supervisor()),
205
208
  presetProvenance: new PresetProvenance(options.configPath),
@@ -26,7 +26,8 @@ import { WebAuth } from './auth.js';
26
26
  import { FleetEventBus } from './events.js';
27
27
  import { ROLE_NAME_RE } from '../config.js';
28
28
  import { TaskListError } from '../rooms-tasks/task-lists.js';
29
- import { acknowledgeTaskProgress, acknowledgeTaskReview, contributionUnread, TaskStateError } from '../rooms-tasks/task-state.js';
29
+ import { acknowledgeTaskProgress, acknowledgeTaskReview, contributionUnread, resolveTaskRef, TaskStateError } from '../rooms-tasks/task-state.js';
30
+ import { MissionKeyError } from '../rooms-tasks/mission-keys.js';
30
31
  import { appendTaskProgress, getTaskReview, taskProgressSnapshot, publicProgressEntry, publicReview, readTaskReviewArtifact, setTaskReviewMarkdown, taskContributionSummary, TaskReviewError, } from '../rooms-tasks/task-review.js';
31
32
  import { isProvider } from '../subscriptions/store.js';
32
33
  const ONBOARDING_WORK = ['coordination', 'development', 'review'];
@@ -321,6 +322,8 @@ export async function buildWebServer(services, boundary, options = {}) {
321
322
  ? 'conflict' : 'invalid_request';
322
323
  throw new FleetError(code, error.message);
323
324
  }
325
+ if (error instanceof MissionKeyError)
326
+ throw new FleetError(error.code === 'invalid_prefix' ? 'invalid_request' : 'conflict', error.message, { details: { reason: error.code } });
324
327
  if (error instanceof TaskStateError && 'accepted' in error && error.accepted === false)
325
328
  return Promise.reject(Object.assign(new FleetError('invalid_request', error.message), { memberNotAccepted: true }));
326
329
  if (error instanceof TaskStateError) {
@@ -329,6 +332,20 @@ export async function buildWebServer(services, boundary, options = {}) {
329
332
  throw error;
330
333
  }
331
334
  };
335
+ /** Canonical task ID for a `:id` param holding an ID or a mission key; call only after authentication. */
336
+ const taskRef = (request, options = {}) => {
337
+ const ref = String(request.params.id ?? '');
338
+ try {
339
+ return resolveTaskRef(ref, options);
340
+ }
341
+ catch (error) {
342
+ if (error instanceof MissionKeyError)
343
+ throw new FleetError('resource_not_found', error.message);
344
+ if (error instanceof TaskStateError)
345
+ throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'invalid_request', error.message);
346
+ throw error;
347
+ }
348
+ };
332
349
  const reviewApi = async (fn) => taskApi(async () => {
333
350
  try {
334
351
  return await fn();
@@ -345,7 +362,7 @@ export async function buildWebServer(services, boundary, options = {}) {
345
362
  });
346
363
  /** Browser task payloads carry Progress/To review counts; bodies have their own routes. */
347
364
  const wireTask = (task) => {
348
- const { progress: _progress, review: _review, ...rest } = task;
365
+ const { progress: _progress, review: _review, member_naming: _naming, ...rest } = task;
349
366
  return { ...rest, ...taskContributionSummary(task) };
350
367
  };
351
368
  const wireTaskResult = (result) => {
@@ -517,15 +534,29 @@ export async function buildWebServer(services, boundary, options = {}) {
517
534
  });
518
535
  app.post('/api/v1/task-lists', async (request, reply) => {
519
536
  auth.authenticate(request, true);
520
- const name = String(request.body?.name ?? '');
521
- const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name }));
537
+ const body = request.body;
538
+ const name = String(body?.name ?? '');
539
+ if (body?.prefix !== undefined && typeof body.prefix !== 'string')
540
+ throw new FleetError('invalid_request', 'prefix must be a string');
541
+ const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name,
542
+ prefix: body?.prefix }));
522
543
  reply.code(201);
523
544
  return { list };
524
545
  });
546
+ app.get('/api/v1/task-lists/prefix-suggestion', async (request) => {
547
+ auth.authenticate(request);
548
+ const name = String(request.query.name ?? '');
549
+ return { prefix: await taskApi(() => requireTaskRooms().suggestTaskListPrefix(name)) };
550
+ });
525
551
  app.patch('/api/v1/task-lists/:name', async (request) => {
526
552
  auth.authenticate(request, true);
527
553
  const name = request.params.name;
528
- const newName = String(request.body?.name ?? '');
554
+ const body = request.body;
555
+ if (body?.prefix !== undefined && typeof body.prefix !== 'string')
556
+ throw new FleetError('invalid_request', 'prefix must be a string');
557
+ if (body?.name === undefined && typeof body?.prefix === 'string')
558
+ return { list: await taskApi(() => requireTaskRooms().setTaskListPrefix({ actor: { kind: 'local_control', surface: 'web' }, name, prefix: body.prefix })) };
559
+ const newName = String(body?.name ?? '');
529
560
  return { list: await taskApi(() => requireTaskRooms().renameTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, newName })) };
530
561
  });
531
562
  app.delete('/api/v1/task-lists/:name', async (request) => {
@@ -540,11 +571,13 @@ export async function buildWebServer(services, boundary, options = {}) {
540
571
  });
541
572
  app.delete('/api/v1/tasks/:id', async (request, reply) => {
542
573
  auth.authenticate(request, true);
543
- const taskId = request.params.id;
574
+ const submitted = request.params.id;
544
575
  const confirm = request.query.confirm;
545
- // Exact-ID-twice confirmation is validated before existence or idempotency.
546
- if (confirm !== taskId)
547
- throw new FleetError('invalid_request', 'confirm must exactly repeat the task id');
576
+ // Exact raw ref-twice confirmation is validated before existence or idempotency.
577
+ if (confirm !== submitted)
578
+ throw new FleetError('invalid_request', 'confirm must exactly repeat the task id or key');
579
+ // A deleted key still maps to its canonical ID, so a repeated DELETE stays idempotent.
580
+ const taskId = taskRef(request, { allowDeleted: true });
548
581
  const accepted = await taskApi(() => requireTaskRooms().requestTaskDeletion({
549
582
  actor: { kind: 'local_control', surface: 'web' }, taskId,
550
583
  }));
@@ -559,7 +592,7 @@ export async function buildWebServer(services, boundary, options = {}) {
559
592
  return {
560
593
  task_id: taskId, accepted: true, deletion: 'pending',
561
594
  ...(outcome.error ? { error: outcome.error } : {}),
562
- recovery: `Repeat DELETE /api/v1/tasks/${taskId}?confirm=${taskId}.`,
595
+ recovery: `Repeat DELETE /api/v1/tasks/${submitted}?confirm=${submitted}.`,
563
596
  };
564
597
  });
565
598
  app.post('/api/v1/tasks', async (request, reply) => {
@@ -588,7 +621,7 @@ export async function buildWebServer(services, boundary, options = {}) {
588
621
  });
589
622
  app.patch('/api/v1/tasks/:id/list', async (request) => {
590
623
  auth.authenticate(request, true);
591
- const taskId = request.params.id;
624
+ const taskId = taskRef(request);
592
625
  const list = String(request.body?.list ?? '');
593
626
  return { task: wireTask(await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list }))) };
594
627
  });
@@ -598,17 +631,17 @@ export async function buildWebServer(services, boundary, options = {}) {
598
631
  if (typeof body?.brief !== 'string' || typeof body.expectedBrief !== 'string' || body.brief.length > 100_000)
599
632
  throw new FleetError('invalid_request', 'brief and expectedBrief must be strings; brief maximum is 100000 characters');
600
633
  return { task: wireTask(await taskApi(() => requireTaskRooms().editTaskDescription({
601
- actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
634
+ actor: { kind: 'local_control', surface: 'web' }, taskId: taskRef(request),
602
635
  brief: body.brief, expectedBrief: body.expectedBrief,
603
636
  }))) };
604
637
  });
605
638
  app.get('/api/v1/tasks/:id', async (request) => {
606
639
  auth.authenticate(request);
607
- return wireTaskResult(await taskApi(() => requireTaskRooms().getTask(request.params.id)));
640
+ return wireTaskResult(await taskApi(() => requireTaskRooms().getTask(taskRef(request))));
608
641
  });
609
642
  app.get('/api/v1/tasks/:id/progress', async (request) => {
610
643
  auth.authenticate(request);
611
- return { progress: await reviewApi(() => taskProgressSnapshot(request.params.id)) };
644
+ return { progress: await reviewApi(() => taskProgressSnapshot(taskRef(request))) };
612
645
  });
613
646
  app.post('/api/v1/tasks/:id/progress', async (request, reply) => {
614
647
  auth.authenticate(request, true);
@@ -617,7 +650,7 @@ export async function buildWebServer(services, boundary, options = {}) {
617
650
  const requestKey = typeof body?.requestKey === 'string' ? body.requestKey : typeof header === 'string' ? header : undefined;
618
651
  if (typeof body?.text !== 'string' || (body.requestKey !== undefined && typeof body.requestKey !== 'string'))
619
652
  throw new FleetError('invalid_request', 'text must be a string; requestKey, when given, a string');
620
- const result = await reviewApi(() => appendTaskProgress(request.params.id, {
653
+ const result = await reviewApi(() => appendTaskProgress(taskRef(request), {
621
654
  text: body.text, requestKey, author: { kind: 'local_control', surface: 'web' },
622
655
  }));
623
656
  if (!result.replayed)
@@ -630,7 +663,7 @@ export async function buildWebServer(services, boundary, options = {}) {
630
663
  const ids = request.body?.ids;
631
664
  if (!Array.isArray(ids) || ids.length > 200 || ids.some(id => typeof id !== 'string' || id.length > 64))
632
665
  throw new FleetError('invalid_request', 'ids must be at most 200 progress entry IDs');
633
- return { unread: await reviewApi(() => acknowledgeTaskProgress(request.params.id, ids)) };
666
+ return { unread: await reviewApi(() => acknowledgeTaskProgress(taskRef(request), ids)) };
634
667
  });
635
668
  /** Acknowledge the To review revision the browser fetched and rendered. */
636
669
  app.post('/api/v1/tasks/:id/review/read', async (request) => {
@@ -638,11 +671,11 @@ export async function buildWebServer(services, boundary, options = {}) {
638
671
  const revision = request.body?.revision;
639
672
  if (!Number.isSafeInteger(revision) || revision < 0)
640
673
  throw new FleetError('invalid_request', 'revision must be a non-negative integer');
641
- return { unread: await reviewApi(() => acknowledgeTaskReview(request.params.id, revision)) };
674
+ return { unread: await reviewApi(() => acknowledgeTaskReview(taskRef(request), revision)) };
642
675
  });
643
676
  app.get('/api/v1/tasks/:id/review', async (request) => {
644
677
  auth.authenticate(request);
645
- const review = await reviewApi(() => getTaskReview(request.params.id));
678
+ const review = await reviewApi(() => getTaskReview(taskRef(request)));
646
679
  return { review: review ? publicReview(review) : null };
647
680
  });
648
681
  app.put('/api/v1/tasks/:id/review', async (request) => {
@@ -650,7 +683,7 @@ export async function buildWebServer(services, boundary, options = {}) {
650
683
  const body = request.body;
651
684
  if (typeof body?.markdown !== 'string' || typeof body.expectedMarkdown !== 'string')
652
685
  throw new FleetError('invalid_request', 'markdown and expectedMarkdown must be strings');
653
- const review = await reviewApi(() => setTaskReviewMarkdown(request.params.id, {
686
+ const review = await reviewApi(() => setTaskReviewMarkdown(taskRef(request), {
654
687
  markdown: body.markdown, expectedMarkdown: body.expectedMarkdown,
655
688
  author: { kind: 'local_control', surface: 'web' },
656
689
  }));
@@ -658,7 +691,7 @@ export async function buildWebServer(services, boundary, options = {}) {
658
691
  });
659
692
  app.get('/api/v1/tasks/:id/review/artifacts/:artifactId', async (request, reply) => {
660
693
  auth.authenticate(request);
661
- const { artifact, bytes } = await reviewApi(() => readTaskReviewArtifact(request.params.id, request.params.artifactId));
694
+ const { artifact, bytes } = await reviewApi(() => readTaskReviewArtifact(taskRef(request), request.params.artifactId));
662
695
  reply.header('Cache-Control', 'private, no-store');
663
696
  reply.header('X-Content-Type-Options', 'nosniff');
664
697
  reply.header('Content-Security-Policy', "default-src 'none'; sandbox");
@@ -669,7 +702,7 @@ export async function buildWebServer(services, boundary, options = {}) {
669
702
  auth.authenticate(request, true);
670
703
  const body = request.body;
671
704
  try {
672
- const result = await taskApi(() => requireTaskRooms().addMember(request.params.id, body));
705
+ const result = await taskApi(() => requireTaskRooms().addMember(taskRef(request), body));
673
706
  reply.code(result.state === 'running' ? 202 : 200);
674
707
  return result;
675
708
  }
@@ -681,7 +714,8 @@ export async function buildWebServer(services, boundary, options = {}) {
681
714
  });
682
715
  app.get('/api/v1/tasks/:id/member-additions/:requestId', async (request) => {
683
716
  auth.authenticate(request);
684
- const { id, requestId } = request.params;
717
+ const { requestId } = request.params;
718
+ const id = taskRef(request);
685
719
  return taskApi(() => requireTaskRooms().memberAddition(id, requestId));
686
720
  });
687
721
  app.patch('/api/v1/tasks/:id/layout', async (request) => {
@@ -691,18 +725,19 @@ export async function buildWebServer(services, boundary, options = {}) {
691
725
  if (!name(body?.layout) || !(body.expectedLayout === null || name(body.expectedLayout)))
692
726
  throw new FleetError('invalid_request', 'layout must be a layout name; expectedLayout a layout name or null');
693
727
  const task = await taskApi(() => requireTaskRooms().setTaskLayout({ actor: { kind: 'local_control', surface: 'web' },
694
- taskId: request.params.id, layout: body.layout, expectedLayout: body.expectedLayout }));
728
+ taskId: taskRef(request), layout: body.layout, expectedLayout: body.expectedLayout }));
695
729
  await audit.record({ requestId: request.id, browser: session.id, action: 'task.layout.set', result: 'succeeded' });
696
730
  return { task: wireTask(requireTaskRooms().withLayoutRooms(task)) };
697
731
  });
698
732
  app.get('/api/v1/tasks/:id/layout', async (request) => {
699
733
  auth.authenticate(request);
700
- return { layout: await taskApi(async () => requireTaskRooms().taskLayout(request.params.id)) };
734
+ return { layout: await taskApi(async () => requireTaskRooms().taskLayout(taskRef(request))) };
701
735
  });
702
736
  for (const operation of ['open', 'close']) {
703
737
  app.post(`/api/v1/tasks/:id/layout/rooms/:room/${operation}`, async (request, reply) => {
704
738
  const session = auth.authenticate(request, true);
705
- const { id, room } = request.params;
739
+ const { room } = request.params;
740
+ const id = taskRef(request);
706
741
  const accepted = await taskApi(() => requireTaskRooms().launchTaskLayoutOperation({
707
742
  actor: { kind: 'local_control', surface: 'web' }, taskId: id, room,
708
743
  operation: operation === 'open' ? 'open' : 'close-room',
@@ -715,7 +750,7 @@ export async function buildWebServer(services, boundary, options = {}) {
715
750
  app.post('/api/v1/tasks/:id/layout/cleanup', async (request, reply) => {
716
751
  auth.authenticate(request, true);
717
752
  const accepted = await taskApi(() => requireTaskRooms().retryTaskLayoutCleanup({
718
- actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
753
+ actor: { kind: 'local_control', surface: 'web' }, taskId: taskRef(request),
719
754
  }));
720
755
  reply.code(202);
721
756
  return { operation: accepted };
@@ -761,7 +796,7 @@ export async function buildWebServer(services, boundary, options = {}) {
761
796
  for (const action of ['start', 'block', 'unblock', 'review', 'finish', 'cancel']) {
762
797
  app.post(`/api/v1/tasks/:id/${action}`, async (request, reply) => {
763
798
  auth.authenticate(request, true);
764
- const taskId = request.params.id;
799
+ const taskId = taskRef(request);
765
800
  const body = (request.body ?? {});
766
801
  const input = { actor: { kind: 'local_control', surface: 'web' }, taskId };
767
802
  const api = requireTaskRooms();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.2.5",
3
+ "version": "2.3.0",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -49,9 +49,9 @@
49
49
  "dependencies": {
50
50
  "@agentclientprotocol/sdk": "^1.3.0",
51
51
  "@fastify/websocket": "^11.2.0",
52
- "@ours.network/cli": "4.1.0",
53
- "@ours.network/mcp": "2.0.2",
54
- "@ours.network/sdk": "5.1.0",
52
+ "@ours.network/cli": "4.2.0",
53
+ "@ours.network/mcp": "2.0.3",
54
+ "@ours.network/sdk": "5.2.0",
55
55
  "commander": "^12.1.0",
56
56
  "fastify": "^5.4.0",
57
57
  "qrcode": "1.5.4",
@@ -65,7 +65,7 @@
65
65
  "@agentclientprotocol/codex-acp": "1.10.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@ours.network/daemon": "5.1.0",
68
+ "@ours.network/daemon": "5.2.0",
69
69
  "@types/node": "^22.20.4",
70
70
  "@types/qrcode": "^1.5.6",
71
71
  "@types/ws": "^8.18.1",