@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21

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 (202) hide show
  1. package/README.md +204 -55
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +6 -25
  8. package/dist/application/role-creation-service.js +34 -72
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +58 -6
  14. package/dist/application/task-room-service.js +435 -79
  15. package/dist/briefing.js +74 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +241 -76
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +75 -3
  22. package/dist/config.js +274 -34
  23. package/dist/creation.d.ts +1 -1
  24. package/dist/creation.js +3 -2
  25. package/dist/daemon-recovery.d.ts +93 -0
  26. package/dist/daemon-recovery.js +328 -0
  27. package/dist/docs.d.ts +1 -1
  28. package/dist/docs.js +192 -66
  29. package/dist/doctor.d.ts +2 -0
  30. package/dist/doctor.js +54 -4
  31. package/dist/fleet-command-audit.d.ts +165 -0
  32. package/dist/fleet-command-audit.js +555 -0
  33. package/dist/fleet-proxy.d.ts +7 -2
  34. package/dist/fleet-proxy.js +30 -8
  35. package/dist/harness/agent-session.d.ts +5 -0
  36. package/dist/harness/claude-code-session.d.ts +2 -0
  37. package/dist/harness/claude-code-session.js +6 -1
  38. package/dist/harness/claude-code.js +2 -0
  39. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  40. package/dist/harness/codex-app-server-proxy.js +348 -14
  41. package/dist/harness/codex-session.d.ts +11 -2
  42. package/dist/harness/codex-session.js +22 -2
  43. package/dist/harness/codex.d.ts +5 -1
  44. package/dist/harness/codex.js +110 -10
  45. package/dist/harness/types.d.ts +1 -1
  46. package/dist/index.d.ts +5 -4
  47. package/dist/index.js +4 -3
  48. package/dist/init-guidance.d.ts +1 -1
  49. package/dist/init-guidance.js +4 -3
  50. package/dist/init-wizard.d.ts +98 -0
  51. package/dist/init-wizard.js +629 -0
  52. package/dist/lifecycle-summary.d.ts +134 -0
  53. package/dist/lifecycle-summary.js +179 -0
  54. package/dist/loops/config.d.ts +10 -1
  55. package/dist/loops/config.js +38 -5
  56. package/dist/model-env.d.ts +2 -2
  57. package/dist/model-env.js +1 -1
  58. package/dist/model-recovery.d.ts +1 -1
  59. package/dist/ops.js +2 -17
  60. package/dist/owner-channel/channel.d.ts +58 -0
  61. package/dist/owner-channel/channel.js +321 -42
  62. package/dist/owner-channel/commands.d.ts +6 -1
  63. package/dist/owner-channel/commands.js +34 -19
  64. package/dist/owner-channel/state.d.ts +2 -0
  65. package/dist/owner-channel/state.js +8 -2
  66. package/dist/permissions.js +1 -1
  67. package/dist/preset-bootstrap.d.ts +10 -0
  68. package/dist/preset-bootstrap.js +85 -0
  69. package/dist/preset-migration.d.ts +42 -0
  70. package/dist/preset-migration.js +414 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.js +13 -1
  84. package/dist/rooms-tasks/cli.d.ts +2 -0
  85. package/dist/rooms-tasks/cli.js +514 -75
  86. package/dist/rooms-tasks/close.d.ts +9 -1
  87. package/dist/rooms-tasks/close.js +20 -4
  88. package/dist/rooms-tasks/config.js +16 -14
  89. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  90. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  91. package/dist/rooms-tasks/deletion.d.ts +51 -0
  92. package/dist/rooms-tasks/deletion.js +217 -0
  93. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  94. package/dist/rooms-tasks/external-worker.js +14 -9
  95. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  96. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  97. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  98. package/dist/rooms-tasks/member-overrides.js +204 -0
  99. package/dist/rooms-tasks/provision.js +184 -49
  100. package/dist/rooms-tasks/room-state.d.ts +1 -0
  101. package/dist/rooms-tasks/room-state.js +12 -1
  102. package/dist/rooms-tasks/task-state.d.ts +115 -3
  103. package/dist/rooms-tasks/task-state.js +361 -11
  104. package/dist/rooms-tasks/templates.d.ts +4 -2
  105. package/dist/rooms-tasks/templates.js +42 -78
  106. package/dist/rooms-tasks/terminal.d.ts +3 -0
  107. package/dist/rooms-tasks/terminal.js +7 -3
  108. package/dist/rooms-tasks/types.d.ts +96 -17
  109. package/dist/rooms-tasks/types.js +14 -4
  110. package/dist/runner.d.ts +12 -2
  111. package/dist/runner.js +124 -43
  112. package/dist/session/acp.d.ts +9 -1
  113. package/dist/session/acp.js +35 -9
  114. package/dist/session/arbiter.d.ts +2 -1
  115. package/dist/session/arbiter.js +2 -0
  116. package/dist/session/codex-app-server-transport.d.ts +53 -0
  117. package/dist/session/codex-app-server-transport.js +184 -0
  118. package/dist/session/codex-app-server.d.ts +122 -0
  119. package/dist/session/codex-app-server.js +1031 -0
  120. package/dist/session/control.d.ts +14 -1
  121. package/dist/session/control.js +38 -0
  122. package/dist/session/conversation-types.d.ts +4 -4
  123. package/dist/session/types.d.ts +24 -6
  124. package/dist/session/types.js +28 -0
  125. package/dist/spawn.d.ts +15 -31
  126. package/dist/spawn.js +117 -237
  127. package/dist/temp-lifecycle.d.ts +1 -1
  128. package/dist/watchdog/config.d.ts +10 -7
  129. package/dist/watchdog/config.js +29 -19
  130. package/dist/watchdog/run.js +4 -13
  131. package/dist/web/fleet-config-service.d.ts +1 -0
  132. package/dist/web/fleet-config-service.js +27 -8
  133. package/dist/web/runtime.js +11 -6
  134. package/dist/web/server.js +28 -1
  135. package/dist/web/topology-promote.js +8 -1
  136. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  137. package/dist/web-app/index.html +1 -1
  138. package/package.json +3 -2
  139. package/presets/brain-catalog.json +16 -0
  140. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  141. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  142. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  143. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  144. package/presets/fleet/brains/claude-default.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  147. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  148. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  149. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  152. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  153. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  154. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  157. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  158. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  159. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  190. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  191. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  192. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  193. package/presets/fleet/roles/Coordinator.yaml +30 -0
  194. package/presets/fleet/roles/Critic.yaml +19 -0
  195. package/presets/fleet/roles/Developer.yaml +19 -0
  196. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  197. package/presets/fleet/room_templates/pair.yaml +10 -0
  198. package/presets/fleet/room_templates/single.yaml +7 -0
  199. package/presets/fleet/room_templates/team.yaml +12 -0
  200. package/presets/fleet.yaml +7 -0
  201. package/presets/manifest.json +6 -0
  202. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -1,14 +1,21 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { ConfigError, loadConfig } from '../config.js';
3
4
  import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
4
5
  import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
5
6
  import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
6
- import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
7
+ import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
7
8
  import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
8
- import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
9
+ import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
10
+ import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
11
+ import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
12
+ import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
9
13
  import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
10
- import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
11
- import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
14
+ import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
15
+ import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
16
+ import { withFileLock } from '../atomic-file.js';
17
+ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
18
+ import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
12
19
  export class TaskRoomApplicationError extends Error {
13
20
  code;
14
21
  fields;
@@ -19,6 +26,53 @@ export class TaskRoomApplicationError extends Error {
19
26
  this.name = 'TaskRoomApplicationError';
20
27
  }
21
28
  }
29
+ export function resolveRoomLaunchPolicy(template, override) {
30
+ const anonymous = override ?? template?.room?.anonymous ?? false;
31
+ return { anonymous };
32
+ }
33
+ function launchSelection(definition, key) {
34
+ const selected = definition?.[key];
35
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
36
+ return undefined;
37
+ const value = selected;
38
+ if (value.kind === 'ref' && typeof value.id === 'string')
39
+ return `ref:${value.id} (reference)`;
40
+ if (value.kind === 'inline' && typeof value.fingerprint === 'string')
41
+ return `inline:${value.fingerprint} (inline)`;
42
+ return undefined;
43
+ }
44
+ function launchPermissions(definition) {
45
+ const selected = definition?.permissions;
46
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
47
+ return undefined;
48
+ const value = selected;
49
+ const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof value[key] === 'string' ? [`${key}=${value[key]}`] : []);
50
+ return fields.length ? fields.join(',') : undefined;
51
+ }
52
+ function roomParticipants(room) {
53
+ return [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(seat => ({
54
+ name: seat.role_name, id: seat.identity_cid,
55
+ brain: launchSelection(seat.launch?.agent_definition, 'brain'),
56
+ role: launchSelection(seat.launch?.agent_definition, 'role') ?? seat.cowork_role,
57
+ permissions: launchPermissions(seat.launch?.agent_definition),
58
+ ...(seat.launch?.presentation ? { configuration: seat.launch.presentation } : {}),
59
+ }));
60
+ }
61
+ function taskAgents(task) {
62
+ const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
63
+ const seats = new Map(room?.member_seats.map(seat => [seat.role_name, seat]) ?? []);
64
+ return [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => {
65
+ const seat = seats.get(member.name);
66
+ return { name: member.name, brain: launchSelection(seat?.launch?.agent_definition, 'brain'),
67
+ role: launchSelection(seat?.launch?.agent_definition, 'role') ?? member.cowork_role,
68
+ permissions: launchPermissions(seat?.launch?.agent_definition),
69
+ ...(seat?.launch?.presentation ? { configuration: seat.launch.presentation } : {}) };
70
+ });
71
+ }
72
+ function roomFailureEventId(room) {
73
+ const evidence = `${room.created_at}\0${room.saga.phase}\0${room.saga.step_index}\0${room.saga.error ?? ''}`;
74
+ return `sha256:${createHash('sha256').update(evidence).digest('hex')}`;
75
+ }
22
76
  /** Exact extraction of the previously CLI-owned task create/start behavior. */
23
77
  export class TaskRoomApplicationService {
24
78
  configurationPath;
@@ -30,29 +84,78 @@ export class TaskRoomApplicationService {
30
84
  }
31
85
  async createTask(request) {
32
86
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
33
- const template = this.createTemplate(cfg, request.template, request.noRoom);
34
- const ref = template && {
35
- name: template.name, version: template.version, content_hash: template.content_hash,
36
- };
87
+ let template = this.createTemplate(cfg, request.template, request.noRoom);
88
+ if (request.noRoom && request.anonymous !== undefined)
89
+ throw new ConfigError('--anonymous/--no-anonymous cannot be combined with --no-room');
90
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
91
+ let launchDefinitions;
92
+ let executionPlan;
93
+ let preparedPlan;
94
+ if (template) {
95
+ const definition = resolveTemplate(template.name, cfg.roomTemplates ?? {});
96
+ preparedPlan = prepareExecutionPlan(definition, cfg, request.members ?? {});
97
+ launchDefinitions = preparedPlan.launchDefinitions;
98
+ }
37
99
  const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
38
- let task = await withTaskListsLock(() => {
39
- const list = resolveTaskList(request.list ?? 'default');
40
- return persistTask({
41
- title: request.title, brief, brief_file: request.briefFile, template: ref,
42
- origin: request.origin, idempotency_key: request.idempotencyKey,
43
- start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
100
+ const unlock = preparedPlan ? acquireLaunchSnapshotLock() : undefined;
101
+ let sealedHash;
102
+ let task;
103
+ try {
104
+ if (preparedPlan) {
105
+ template = sealTemplateSnapshot(preparedPlan.snapshot, cfg.agentTemplates ?? {}, preparedPlan.launchDefinitions);
106
+ sealedHash = template.launch_snapshot_hash;
107
+ executionPlan = { schema_version: 1, snapshot: template, room_policy: roomPolicy,
108
+ overrides: preparedPlan.overrides,
109
+ overrides_hash: preparedPlan.overridesHash,
110
+ plan_hash: preparedPlan.planHash };
111
+ }
112
+ const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
113
+ task = await withTaskListsLock(() => {
114
+ const list = resolveTaskList(request.list ?? 'default');
115
+ return persistTask({
116
+ title: request.title, brief, brief_file: request.briefFile, template: ref,
117
+ execution_plan: executionPlan,
118
+ origin: request.origin, idempotency_key: request.idempotencyKey,
119
+ start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
120
+ });
44
121
  });
45
- });
122
+ }
123
+ catch (error) {
124
+ unlock?.();
125
+ if (sealedHash)
126
+ releaseLaunchSnapshot(sealedHash);
127
+ throw error;
128
+ }
129
+ unlock?.();
130
+ const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
131
+ recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
132
+ title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
133
+ list: task.list_name ?? 'default', roomId: task.room_id,
134
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
135
+ agents: [] });
46
136
  if (!request.backlog && !request.noRoom && ref && !task.room_id) {
47
137
  try {
48
- await this.provisionRoom(cfg, task, template, room => {
49
- task = updateTaskRoom(task.task_id, room.room_id, room.room_identity_cid);
50
- });
138
+ const room = await this.provisionRoom(cfg, task, template, created => {
139
+ task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
140
+ }, launchDefinitions, roomPolicy);
51
141
  task = readTask(task.task_id);
142
+ if (task.state === 'active' && room.state === 'active')
143
+ recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
144
+ title: task.title, previousState: 'provisioning', newState: task.state,
145
+ revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
146
+ list: task.list_name ?? 'default', roomId: task.room_id,
147
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
148
+ agents: taskAgents(task) });
149
+ else
150
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
151
+ state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
52
152
  }
53
153
  catch (error) {
54
154
  if (error instanceof CoworkUnavailableError)
55
155
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
156
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
157
+ id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
158
+ eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
56
159
  throw error;
57
160
  }
58
161
  }
@@ -66,27 +169,25 @@ export class TaskRoomApplicationService {
66
169
  const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
67
170
  if (!definition)
68
171
  throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
69
- template = snapshotTemplate(definition);
172
+ if (request.members && Object.keys(request.members).length) {
173
+ const prepared = prepareExecutionPlan(definition, cfg, request.members);
174
+ template = prepared.snapshot;
175
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
176
+ return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions, roomPolicy);
177
+ }
178
+ template = snapshotTemplate(definition, cfg.agentTemplates);
70
179
  }
180
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
71
181
  return this.provisionRoom(cfg, {
72
182
  title: request.name, brief, goal: request.goal,
73
- }, template, () => { });
183
+ }, template, () => { }, undefined, roomPolicy);
74
184
  }
75
185
  async startTask(input) {
76
- const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
77
- let task = transitionTask(input.taskId);
78
- if (task.template && !task.room_id) {
79
- const template = this.existingTemplate(cfg, task);
80
- await this.provisionRoom(cfg, task, template, room => {
81
- task = updateTaskRoom(task.task_id, room.room_id, room.room_identity_cid);
82
- });
83
- task = readTask(task.task_id);
84
- }
85
- return task;
186
+ return (await this.ensureTaskWork(input)).task;
86
187
  }
87
188
  listTasks(filter) {
88
189
  const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
89
- return readTasks({ state: filter?.state, listId });
190
+ return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
90
191
  }
91
192
  listTaskLists() { return readTaskLists(); }
92
193
  groupedTasks(filter) {
@@ -139,8 +240,93 @@ export class TaskRoomApplicationService {
139
240
  reviewTask(input) {
140
241
  return persistReviewTask(input.taskId);
141
242
  }
142
- deleteTask(input) {
143
- return persistDeleteTask(input.taskId);
243
+ deletionActor(actor) {
244
+ if (actor.kind === 'local_control')
245
+ return { kind: 'local_control', surface: actor.surface };
246
+ if (actor.kind === 'authenticated_owner')
247
+ return { kind: 'authenticated_owner', surface: 'messenger', cid: actor.cid };
248
+ throw new Error('internal workers cannot originate a task deletion acceptance');
249
+ }
250
+ /** Accept a permanent deletion in any lifecycle state; audit at acceptance. */
251
+ async requestTaskDeletion(input) {
252
+ const result = await acceptTaskDeletion(input.taskId, this.deletionActor(input.actor));
253
+ if (result.status === 'accepted') {
254
+ const task = result.task;
255
+ recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: task.task_id,
256
+ title: task.title, previousState: task.state, newState: 'deleting',
257
+ revision: task.deletion?.accepted_at ?? task.created_at, list: task.list_name ?? 'default',
258
+ roomId: task.room_id,
259
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
260
+ agents: [] });
261
+ }
262
+ return result;
263
+ }
264
+ /** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
265
+ async settleTaskDeletion(input) {
266
+ const result = await settleTaskDeletion({
267
+ taskId: input.taskId,
268
+ cowork: () => {
269
+ // Config resolves only when room work exists: a no-room deletion must
270
+ // settle even with missing or invalid rooms configuration.
271
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
272
+ if (!cfg.rooms)
273
+ throw new ConfigError('rooms: configuration is required before deleting task rooms');
274
+ return this.deps.cowork ? this.deps.cowork(cfg)
275
+ : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
276
+ },
277
+ });
278
+ if (result.deleted && result.previous_state) {
279
+ recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: result.task_id,
280
+ title: result.title, previousState: result.previous_state, newState: 'deleted',
281
+ revision: new Date().toISOString(), list: 'default', roomId: undefined,
282
+ template: undefined, agents: [] });
283
+ }
284
+ return result;
285
+ }
286
+ /**
287
+ * Launch the external deletion worker outside the caller's lifecycle and
288
+ * wait boundedly for physical absence. Timeouts and launch failures report
289
+ * a pending, recoverable state — never success. A concurrent worker that
290
+ * already removed the record reads as settled.
291
+ */
292
+ async launchTaskDeletionWorker(input) {
293
+ const launch = this.deps.launchDeletionWorker
294
+ ?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
295
+ const errorAtBefore = (() => {
296
+ try {
297
+ return getDeletingTask(input.taskId).deletion?.error_at;
298
+ }
299
+ catch {
300
+ return undefined;
301
+ }
302
+ })();
303
+ try {
304
+ await launch(input.taskId);
305
+ }
306
+ catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ await recordTaskDeletionError(input.taskId, message, `External delete worker failed to start. Repeat the delete request for ${input.taskId}.`).catch(() => { });
309
+ return { deleted: false, pending: true, error: message };
310
+ }
311
+ const now = this.deps.now ?? Date.now;
312
+ const sleep = this.deps.sleep
313
+ ?? ((ms) => new Promise(resolve => { setTimeout(resolve, ms); }));
314
+ const deadline = now() + (input.waitMs ?? 10_000);
315
+ while (now() < deadline) {
316
+ if (taskDeletionState(input.taskId) === 'absent')
317
+ return { deleted: true, pending: false };
318
+ try {
319
+ const current = getDeletingTask(input.taskId).deletion;
320
+ if (current?.error_at !== errorAtBefore && current?.error)
321
+ return { deleted: false, pending: true, error: current.error };
322
+ }
323
+ catch { /* re-checked as absence on the next iteration */ }
324
+ await sleep(100);
325
+ }
326
+ return { deleted: false, pending: true };
327
+ }
328
+ recordDeletionError(input) {
329
+ return recordTaskDeletionError(input.taskId, input.error, input.recoveryHint);
144
330
  }
145
331
  async completeTask(input) {
146
332
  const task = readTask(input.taskId);
@@ -179,10 +365,22 @@ export class TaskRoomApplicationService {
179
365
  return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
180
366
  }
181
367
  async beginTaskRecovery(input) {
368
+ // The deletion-vs-terminal routing decision serializes on the common
369
+ // task-operation lock and must not require configuration: recovery of a
370
+ // deletion-pending task continues the deletion and never resurrects.
371
+ const routed = await withFileLock(taskOperationLockPath(input.taskId), () => {
372
+ const task = readTask(input.taskId);
373
+ if (task.deletion?.status === 'pending')
374
+ return 'deletion';
375
+ if (task.terminal_intent?.status === 'pending')
376
+ return 'terminal';
377
+ return 'none';
378
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
379
+ if (routed === 'deletion')
380
+ return { kind: 'deletion_worker_required', taskId: input.taskId };
182
381
  const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
183
382
  this.recovery = { taskId: input.taskId, config };
184
- const task = readTask(input.taskId);
185
- if (task.terminal_intent?.status === 'pending')
383
+ if (routed === 'terminal')
186
384
  return { kind: 'terminal_worker_required', taskId: input.taskId };
187
385
  return { kind: 'final', result: await this.continueTaskRecovery({
188
386
  actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
@@ -259,8 +457,11 @@ export class TaskRoomApplicationService {
259
457
  validateTemplates() {
260
458
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
261
459
  return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
262
- const issues = template.members.flatMap(member => cfg.roles.some(role => role.name === member.role_ref)
263
- ? [] : [`member ${member.slot}: role_ref '${member.role_ref}' not found in fleet roles`]);
460
+ const issues = template.members.flatMap(member => {
461
+ const ref = member.agent_template;
462
+ return cfg.agentTemplates?.[ref]
463
+ ? [] : [`member ${member.slot}: Agent Template '${ref}' not found`];
464
+ });
264
465
  return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
265
466
  });
266
467
  }
@@ -341,6 +542,14 @@ export class TaskRoomApplicationService {
341
542
  return { kind: 'deletion_worker_required', roomId: input.roomId };
342
543
  const room = await adapter.recoverRoom(input.roomId);
343
544
  orchestration = getRoomRecord(input.roomId);
545
+ if (orchestration) {
546
+ const policy = storedRoomLaunchPolicy(orchestration.room_policy);
547
+ if ((room.anonymous ?? false) !== policy.anonymous) {
548
+ const error = `Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
549
+ setSagaError(input.roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then recover the Room without changing its policy.', 'waiting_cowork');
550
+ throw new Error(error);
551
+ }
552
+ }
344
553
  if (orchestration && !orchestration.owner_seat_cid
345
554
  && (orchestration.provisioning_detail === 'waiting_owner_invite'
346
555
  || orchestration.provisioning_detail === 'owner_cid_mismatch')) {
@@ -410,69 +619,146 @@ export class TaskRoomApplicationService {
410
619
  async ensureTaskWork(input) {
411
620
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
412
621
  let task = readTask(input.taskId);
622
+ const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
623
+ const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
624
+ if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
625
+ throw new TaskRoomApplicationError('template_mismatch', 'anonymous override does not match the existing Room launch policy', { room: task.room_id });
413
626
  if (TASK_TERMINAL_STATES.includes(task.state))
414
627
  throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
628
+ const durableTemplate = task.room_id
629
+ ? getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot
630
+ : task.execution_plan?.snapshot;
631
+ const templateRequestMatches = !input.template || (durableTemplate !== undefined
632
+ && (input.template === durableTemplate.name
633
+ || input.template === `${durableTemplate.name}@${durableTemplate.version}`));
634
+ const memberRequestMatches = input.members === undefined || (task.execution_plan !== undefined
635
+ && task.execution_plan.overrides_hash === hashMemberOverrides(input.members));
636
+ const durableRequestMatches = durableTemplate !== undefined
637
+ && templateRequestMatches && memberRequestMatches;
415
638
  if (task.state === 'active' && task.room_id) {
416
- if (input.template) {
417
- const definition = resolveTemplate(input.template, cfg.roomTemplates ?? {});
639
+ if (input.template || input.members) {
640
+ const pinned = durableTemplate;
641
+ if (!pinned)
642
+ throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
643
+ if (durableRequestMatches)
644
+ return { task, status: 'already_active' };
645
+ const templateName = input.template ?? pinned.name;
646
+ const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
418
647
  if (!definition)
419
- throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: input.template });
420
- const requested = snapshotTemplate(definition);
421
- const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.template;
422
- if (pinned && (pinned.name !== requested.name || pinned.content_hash !== requested.content_hash))
648
+ throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
649
+ const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
650
+ const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
651
+ if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
423
652
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
424
- requested: `${requested.name}@${requested.version}`, room: task.room_id,
653
+ requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
425
654
  provisioned: `${pinned.name}@${pinned.version}`
426
655
  });
427
656
  }
428
657
  return { task, status: 'already_active' };
429
658
  }
430
659
  const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
431
- const durable = room?.template_snapshot;
660
+ const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
432
661
  if (durable && (!task.template || task.template.name !== durable.name
433
662
  || task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
434
- throw new Error(`task ${task.task_id} template reference does not match room ${room.room_id}'s durable snapshot`);
663
+ throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
435
664
  const templateName = input.template ?? durable?.name ?? task.template?.name
436
665
  ?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
437
- const definition = durable && !input.template ? undefined
666
+ const storedOverridesMatch = !!(durable && memberRequestMatches);
667
+ const definition = durable && durableRequestMatches ? undefined
438
668
  : resolveTemplate(templateName, cfg.roomTemplates ?? {});
439
- if (input.template && !definition)
669
+ if (input.template && !definition && !durableRequestMatches)
440
670
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
441
671
  if (!durable && !definition)
442
672
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
443
- const snapshot = durable ?? snapshotTemplate(definition);
673
+ let launchDefinitions;
674
+ let preparedPlan = !durable && definition
675
+ ? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
676
+ let snapshot = durable ?? preparedPlan.snapshot;
677
+ const roomPolicy = input.anonymous !== undefined
678
+ ? resolveRoomLaunchPolicy(snapshot, input.anonymous)
679
+ : task.execution_plan
680
+ ? storedRoomLaunchPolicy(task.execution_plan.room_policy)
681
+ : resolveRoomLaunchPolicy(snapshot, undefined);
682
+ if (preparedPlan)
683
+ launchDefinitions = preparedPlan.launchDefinitions;
684
+ if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
685
+ if (!definition)
686
+ throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
687
+ const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
688
+ preparedPlan = prepared;
689
+ if (durable && prepared.planHash !== durable.content_hash)
690
+ throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
691
+ snapshot = prepared.snapshot;
692
+ launchDefinitions = prepared.launchDefinitions;
693
+ }
444
694
  if (durable && input.template && definition) {
445
- const requested = snapshotTemplate(definition);
695
+ const requested = snapshotTemplate(definition, cfg.agentTemplates);
446
696
  if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
447
697
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
448
698
  requested: `${requested.name}@${requested.version}`, room: room.room_id,
449
699
  provisioned: `${durable.name}@${durable.version}`
450
700
  });
451
701
  }
452
- if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
453
- throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
454
- template: `${task.template.name}@${task.template.version}`
455
- });
702
+ if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
703
+ // Pre-execution-plan backlog records used the template-only hash. Accept
704
+ // that exact legacy pin once, then upgrade it to the complete sealed plan.
705
+ const legacyHash = definition
706
+ ? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
707
+ if (task.template.content_hash !== legacyHash)
708
+ throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
709
+ template: `${task.template.name}@${task.template.version}`
710
+ });
711
+ }
456
712
  if (task.room_id && room?.template_snapshot
457
713
  && (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
458
714
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
459
715
  requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
460
716
  provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
461
717
  });
462
- if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
718
+ if (preparedPlan && !task.room_id) {
719
+ const unlock = acquireLaunchSnapshotLock();
720
+ let sealed;
721
+ try {
722
+ sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
723
+ task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
724
+ room_policy: roomPolicy,
725
+ overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
726
+ plan_hash: preparedPlan.planHash });
727
+ snapshot = sealed;
728
+ }
729
+ catch (error) {
730
+ unlock();
731
+ if (sealed?.launch_snapshot_hash)
732
+ releaseLaunchSnapshot(sealed.launch_snapshot_hash);
733
+ throw error;
734
+ }
735
+ unlock();
736
+ }
737
+ else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
738
+ task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
739
+ }
740
+ else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
463
741
  task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
464
- if (task.state === 'backlog')
742
+ if (task.state === 'backlog') {
465
743
  task = transitionTask(task.task_id);
744
+ recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
745
+ title: task.title, previousState: 'backlog', newState: task.state,
746
+ revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
747
+ template: `${snapshot.name}@${snapshot.version}`, agents: [] });
748
+ }
466
749
  if (!task.room_id) {
467
750
  try {
468
751
  await this.provisionRoom(cfg, task, snapshot, created => {
469
752
  task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
470
- });
753
+ }, launchDefinitions, roomPolicy);
471
754
  task = readTask(task.task_id);
472
755
  }
473
756
  catch (error) {
474
757
  if (error instanceof CoworkUnavailableError)
475
758
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
759
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
760
+ id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
761
+ eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
476
762
  throw error;
477
763
  }
478
764
  }
@@ -491,9 +777,23 @@ export class TaskRoomApplicationService {
491
777
  catch (error) {
492
778
  if (error instanceof CoworkUnavailableError)
493
779
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
780
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
781
+ id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
782
+ eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
494
783
  throw error;
495
784
  }
496
785
  }
786
+ const resultingRoom = getRoomRecord(task.room_id);
787
+ if (task.state === 'active' && resultingRoom?.state === 'active')
788
+ recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
789
+ title: task.title, previousState: 'provisioning', newState: task.state,
790
+ revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
791
+ list: task.list_name ?? 'default', roomId: task.room_id,
792
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
793
+ agents: taskAgents(task) });
794
+ else if (resultingRoom)
795
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
796
+ id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
497
797
  return { task, status: 'ready' };
498
798
  }
499
799
  createTemplate(cfg, requested, noRoom) {
@@ -507,17 +807,17 @@ export class TaskRoomApplicationService {
507
807
  throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
508
808
  return undefined;
509
809
  }
510
- return snapshotTemplate(definition);
810
+ return snapshotTemplate(definition, cfg.agentTemplates);
511
811
  }
512
812
  existingTemplate(cfg, task) {
513
813
  const ref = task.template;
514
814
  const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
515
- const snapshot = definition ? snapshotTemplate(definition) : undefined;
815
+ const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
516
816
  if (!snapshot || snapshot.content_hash !== ref.content_hash)
517
817
  throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
518
818
  return snapshot;
519
819
  }
520
- async provisionRoom(cfg, task, template, onCreated) {
820
+ async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
521
821
  const rooms = cfg.rooms;
522
822
  if (!rooms)
523
823
  throw new ConfigError('rooms: configuration is required');
@@ -529,17 +829,52 @@ export class TaskRoomApplicationService {
529
829
  throw new ConfigError('rooms: configuration is required before creating or querying rooms');
530
830
  return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
531
831
  })();
532
- const created = await cowork.createRoom({
533
- room_name: task.title, goal: task.goal?.trim() || task.title,
534
- briefing: task.brief?.trim() || template?.contract?.trim() || task.goal?.trim() || task.title,
535
- quiet_membership: template?.room?.quiet_membership,
536
- anonymous: template?.room?.anonymous,
537
- });
538
- let room = createRoomRecord({
539
- room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
540
- task_id: task.task_id, template_snapshot: template,
541
- });
542
- onCreated(room);
832
+ const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
833
+ let launchTemplate;
834
+ let room;
835
+ try {
836
+ launchTemplate = template ? (template.launch_snapshot_hash ? template
837
+ : sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
838
+ // Room publication window: for a task-bound room, hold the common
839
+ // task-operation lock across remote creation, local record creation, and
840
+ // the task link, so an accepted deletion linearizes strictly before
841
+ // (bounded task_deleting abort) or strictly after (record visible to the
842
+ // deletion scan). Lock order stays launch-snapshot → task-operation.
843
+ const publishRoom = async () => {
844
+ if (task.task_id) {
845
+ const fresh = readTask(task.task_id);
846
+ if (fresh.deletion?.status === 'pending')
847
+ throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
848
+ }
849
+ const created = await cowork.createRoom({
850
+ room_name: task.title, goal: task.goal?.trim() || task.title,
851
+ briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
852
+ quiet_membership: launchTemplate?.room?.quiet_membership,
853
+ anonymous: policy.anonymous,
854
+ });
855
+ const record = createRoomRecord({
856
+ room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
857
+ task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
858
+ });
859
+ onCreated(record);
860
+ return record;
861
+ };
862
+ room = task.task_id
863
+ ? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
864
+ : await publishRoom();
865
+ }
866
+ catch (error) {
867
+ unlockSnapshot?.();
868
+ if (launchTemplate?.launch_snapshot_hash)
869
+ releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
870
+ throw error;
871
+ }
872
+ unlockSnapshot?.();
873
+ recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
874
+ name: room.room_name, previousState: 'none', newState: 'provisioning',
875
+ revision: room.created_at, taskId: room.task_id,
876
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
877
+ participants: [] });
543
878
  room = advanceSaga(room.room_id, 'create_room', 1);
544
879
  if (attachOwner) {
545
880
  try {
@@ -553,18 +888,39 @@ export class TaskRoomApplicationService {
553
888
  const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
554
889
  setSagaError(room.room_id, error instanceof Error ? error.message : String(error), mismatch ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
555
890
  : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
891
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
892
+ id: room.room_id, state: getRoomRecord(room.room_id)?.state ?? 'provisioning',
893
+ category: 'provision_failed', eventId: roomFailureEventId(getRoomRecord(room.room_id) ?? room) });
556
894
  throw error;
557
895
  }
558
896
  }
559
897
  room = advanceSaga(room.room_id, 'create_members', 3);
560
- if (template?.members.length)
561
- return (this.deps.provisionMembers ?? provisionMembers)({
562
- cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
563
- binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
564
- goal: task.task_id ? task.title : task.goal,
565
- });
566
- room = activateRoom(room.room_id);
567
- if (task.task_id)
898
+ if (launchTemplate?.members.length)
899
+ try {
900
+ room = await (this.deps.provisionMembers ?? provisionMembers)({
901
+ cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
902
+ binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
903
+ goal: task.task_id ? task.title : task.goal,
904
+ });
905
+ }
906
+ catch (error) {
907
+ const failed = getRoomRecord(room.room_id) ?? room;
908
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: failed.room_id,
909
+ state: failed.state, category: 'provision_failed', eventId: roomFailureEventId(failed) });
910
+ throw error;
911
+ }
912
+ else
913
+ room = activateRoom(room.room_id);
914
+ if (room.state === 'active')
915
+ recordFleetAuditPresentation({ kind: 'room', operation: 'activate', id: room.room_id,
916
+ name: room.room_name, previousState: 'provisioning', newState: 'active',
917
+ revision: room.activated_at ?? room.created_at, taskId: room.task_id,
918
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
919
+ participants: roomParticipants(room) });
920
+ else
921
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
922
+ state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
923
+ if (task.task_id && !launchTemplate?.members.length)
568
924
  activateTask(task.task_id);
569
925
  return room;
570
926
  }