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

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 (199) hide show
  1. package/README.md +169 -54
  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 +426 -78
  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 +227 -73
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +67 -2
  22. package/dist/config.js +260 -30
  23. package/dist/daemon-recovery.d.ts +93 -0
  24. package/dist/daemon-recovery.js +328 -0
  25. package/dist/docs.d.ts +1 -1
  26. package/dist/docs.js +171 -66
  27. package/dist/doctor.d.ts +2 -0
  28. package/dist/doctor.js +54 -4
  29. package/dist/fleet-command-audit.d.ts +165 -0
  30. package/dist/fleet-command-audit.js +543 -0
  31. package/dist/fleet-proxy.d.ts +7 -2
  32. package/dist/fleet-proxy.js +30 -8
  33. package/dist/harness/agent-session.d.ts +5 -0
  34. package/dist/harness/claude-code-session.d.ts +2 -0
  35. package/dist/harness/claude-code-session.js +6 -1
  36. package/dist/harness/claude-code.js +2 -0
  37. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  38. package/dist/harness/codex-app-server-proxy.js +348 -14
  39. package/dist/harness/codex-session.d.ts +11 -2
  40. package/dist/harness/codex-session.js +22 -2
  41. package/dist/harness/codex.d.ts +5 -1
  42. package/dist/harness/codex.js +110 -10
  43. package/dist/harness/types.d.ts +1 -1
  44. package/dist/index.d.ts +5 -4
  45. package/dist/index.js +4 -3
  46. package/dist/init-guidance.d.ts +1 -1
  47. package/dist/init-guidance.js +4 -3
  48. package/dist/init-wizard.d.ts +98 -0
  49. package/dist/init-wizard.js +629 -0
  50. package/dist/lifecycle-summary.d.ts +118 -0
  51. package/dist/lifecycle-summary.js +161 -0
  52. package/dist/loops/config.js +3 -2
  53. package/dist/model-env.d.ts +2 -2
  54. package/dist/model-env.js +1 -1
  55. package/dist/model-recovery.d.ts +1 -1
  56. package/dist/ops.js +2 -17
  57. package/dist/owner-channel/channel.d.ts +58 -0
  58. package/dist/owner-channel/channel.js +321 -42
  59. package/dist/owner-channel/commands.d.ts +6 -1
  60. package/dist/owner-channel/commands.js +34 -19
  61. package/dist/owner-channel/state.d.ts +2 -0
  62. package/dist/owner-channel/state.js +8 -2
  63. package/dist/permissions.js +1 -1
  64. package/dist/preset-bootstrap.d.ts +10 -0
  65. package/dist/preset-bootstrap.js +85 -0
  66. package/dist/preset-migration.d.ts +42 -0
  67. package/dist/preset-migration.js +414 -0
  68. package/dist/reports/artifact.d.ts +10 -0
  69. package/dist/reports/artifact.js +32 -0
  70. package/dist/reports/index.d.ts +5 -0
  71. package/dist/reports/index.js +5 -0
  72. package/dist/reports/render.d.ts +6 -0
  73. package/dist/reports/render.js +240 -0
  74. package/dist/reports/service.d.ts +42 -0
  75. package/dist/reports/service.js +57 -0
  76. package/dist/reports/tasks.d.ts +52 -0
  77. package/dist/reports/tasks.js +177 -0
  78. package/dist/reports/types.d.ts +315 -0
  79. package/dist/reports/types.js +6 -0
  80. package/dist/resolved-plan.js +4 -0
  81. package/dist/rooms-tasks/cli.d.ts +2 -0
  82. package/dist/rooms-tasks/cli.js +503 -75
  83. package/dist/rooms-tasks/close.d.ts +9 -1
  84. package/dist/rooms-tasks/close.js +20 -4
  85. package/dist/rooms-tasks/config.js +16 -14
  86. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  87. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  88. package/dist/rooms-tasks/deletion.d.ts +51 -0
  89. package/dist/rooms-tasks/deletion.js +217 -0
  90. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  91. package/dist/rooms-tasks/external-worker.js +14 -9
  92. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  93. package/dist/rooms-tasks/launch-snapshot.js +176 -0
  94. package/dist/rooms-tasks/member-overrides.d.ts +27 -0
  95. package/dist/rooms-tasks/member-overrides.js +170 -0
  96. package/dist/rooms-tasks/provision.js +179 -49
  97. package/dist/rooms-tasks/room-state.d.ts +1 -0
  98. package/dist/rooms-tasks/room-state.js +12 -1
  99. package/dist/rooms-tasks/task-state.d.ts +115 -3
  100. package/dist/rooms-tasks/task-state.js +361 -11
  101. package/dist/rooms-tasks/templates.d.ts +4 -2
  102. package/dist/rooms-tasks/templates.js +42 -78
  103. package/dist/rooms-tasks/terminal.d.ts +3 -0
  104. package/dist/rooms-tasks/terminal.js +7 -3
  105. package/dist/rooms-tasks/types.d.ts +94 -17
  106. package/dist/rooms-tasks/types.js +14 -4
  107. package/dist/runner.d.ts +9 -2
  108. package/dist/runner.js +107 -40
  109. package/dist/session/acp.d.ts +9 -1
  110. package/dist/session/acp.js +35 -9
  111. package/dist/session/arbiter.d.ts +2 -1
  112. package/dist/session/arbiter.js +2 -0
  113. package/dist/session/codex-app-server-transport.d.ts +53 -0
  114. package/dist/session/codex-app-server-transport.js +184 -0
  115. package/dist/session/codex-app-server.d.ts +122 -0
  116. package/dist/session/codex-app-server.js +1031 -0
  117. package/dist/session/control.d.ts +14 -1
  118. package/dist/session/control.js +38 -0
  119. package/dist/session/conversation-types.d.ts +4 -4
  120. package/dist/session/types.d.ts +24 -6
  121. package/dist/session/types.js +28 -0
  122. package/dist/spawn.d.ts +6 -31
  123. package/dist/spawn.js +57 -234
  124. package/dist/temp-lifecycle.d.ts +1 -1
  125. package/dist/watchdog/config.d.ts +10 -7
  126. package/dist/watchdog/config.js +29 -19
  127. package/dist/watchdog/run.js +4 -13
  128. package/dist/web/fleet-config-service.d.ts +1 -0
  129. package/dist/web/fleet-config-service.js +27 -8
  130. package/dist/web/runtime.js +11 -6
  131. package/dist/web/server.js +28 -1
  132. package/dist/web/topology-promote.js +8 -1
  133. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  134. package/dist/web-app/index.html +1 -1
  135. package/package.json +3 -2
  136. package/presets/brain-catalog.json +16 -0
  137. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  138. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  139. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  140. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  143. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  144. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  147. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  148. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  149. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  152. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  153. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  154. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  190. package/presets/fleet/roles/Coordinator.yaml +30 -0
  191. package/presets/fleet/roles/Critic.yaml +19 -0
  192. package/presets/fleet/roles/Developer.yaml +19 -0
  193. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  194. package/presets/fleet/room_templates/pair.yaml +10 -0
  195. package/presets/fleet/room_templates/single.yaml +7 -0
  196. package/presets/fleet/room_templates/team.yaml +12 -0
  197. package/presets/fleet.yaml +7 -0
  198. package/presets/manifest.json +6 -0
  199. 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,138 @@ 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 });
415
628
  if (task.state === 'active' && task.room_id) {
416
- if (input.template) {
417
- const definition = resolveTemplate(input.template, cfg.roomTemplates ?? {});
629
+ if (input.template || input.members) {
630
+ const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot;
631
+ if (!pinned)
632
+ throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
633
+ const templateName = input.template ?? pinned.name;
634
+ if ((!input.template || input.template === pinned.name) && input.members
635
+ && task.execution_plan?.overrides_hash === hashMemberOverrides(input.members))
636
+ return { task, status: 'already_active' };
637
+ const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
418
638
  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))
639
+ throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
640
+ const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
641
+ const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
642
+ if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
423
643
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
424
- requested: `${requested.name}@${requested.version}`, room: task.room_id,
644
+ requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
425
645
  provisioned: `${pinned.name}@${pinned.version}`
426
646
  });
427
647
  }
428
648
  return { task, status: 'already_active' };
429
649
  }
430
650
  const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
431
- const durable = room?.template_snapshot;
651
+ const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
432
652
  if (durable && (!task.template || task.template.name !== durable.name
433
653
  || 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`);
654
+ throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
435
655
  const templateName = input.template ?? durable?.name ?? task.template?.name
436
656
  ?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
437
- const definition = durable && !input.template ? undefined
657
+ const storedOverridesMatch = !!(durable && input.members && task.execution_plan?.overrides_hash
658
+ === hashMemberOverrides(input.members));
659
+ const definition = durable && !input.template && (!input.members || storedOverridesMatch) ? undefined
438
660
  : resolveTemplate(templateName, cfg.roomTemplates ?? {});
439
661
  if (input.template && !definition)
440
662
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
441
663
  if (!durable && !definition)
442
664
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
443
- const snapshot = durable ?? snapshotTemplate(definition);
665
+ let launchDefinitions;
666
+ let preparedPlan = !durable && definition
667
+ ? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
668
+ let snapshot = durable ?? preparedPlan.snapshot;
669
+ const roomPolicy = input.anonymous !== undefined
670
+ ? resolveRoomLaunchPolicy(snapshot, input.anonymous)
671
+ : task.execution_plan
672
+ ? storedRoomLaunchPolicy(task.execution_plan.room_policy)
673
+ : resolveRoomLaunchPolicy(snapshot, undefined);
674
+ if (preparedPlan)
675
+ launchDefinitions = preparedPlan.launchDefinitions;
676
+ if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
677
+ if (!definition)
678
+ throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
679
+ const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
680
+ preparedPlan = prepared;
681
+ if (durable && prepared.planHash !== durable.content_hash)
682
+ throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
683
+ snapshot = prepared.snapshot;
684
+ launchDefinitions = prepared.launchDefinitions;
685
+ }
444
686
  if (durable && input.template && definition) {
445
- const requested = snapshotTemplate(definition);
687
+ const requested = snapshotTemplate(definition, cfg.agentTemplates);
446
688
  if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
447
689
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
448
690
  requested: `${requested.name}@${requested.version}`, room: room.room_id,
449
691
  provisioned: `${durable.name}@${durable.version}`
450
692
  });
451
693
  }
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
- });
694
+ if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
695
+ // Pre-execution-plan backlog records used the template-only hash. Accept
696
+ // that exact legacy pin once, then upgrade it to the complete sealed plan.
697
+ const legacyHash = definition
698
+ ? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
699
+ if (task.template.content_hash !== legacyHash)
700
+ throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
701
+ template: `${task.template.name}@${task.template.version}`
702
+ });
703
+ }
456
704
  if (task.room_id && room?.template_snapshot
457
705
  && (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
458
706
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
459
707
  requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
460
708
  provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
461
709
  });
462
- if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
710
+ if (preparedPlan && !task.room_id) {
711
+ const unlock = acquireLaunchSnapshotLock();
712
+ let sealed;
713
+ try {
714
+ sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
715
+ task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
716
+ room_policy: roomPolicy,
717
+ overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
718
+ plan_hash: preparedPlan.planHash });
719
+ snapshot = sealed;
720
+ }
721
+ catch (error) {
722
+ unlock();
723
+ if (sealed?.launch_snapshot_hash)
724
+ releaseLaunchSnapshot(sealed.launch_snapshot_hash);
725
+ throw error;
726
+ }
727
+ unlock();
728
+ }
729
+ else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
730
+ task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
731
+ }
732
+ else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
463
733
  task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
464
- if (task.state === 'backlog')
734
+ if (task.state === 'backlog') {
465
735
  task = transitionTask(task.task_id);
736
+ recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
737
+ title: task.title, previousState: 'backlog', newState: task.state,
738
+ revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
739
+ template: `${snapshot.name}@${snapshot.version}`, agents: [] });
740
+ }
466
741
  if (!task.room_id) {
467
742
  try {
468
743
  await this.provisionRoom(cfg, task, snapshot, created => {
469
744
  task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
470
- });
745
+ }, launchDefinitions, roomPolicy);
471
746
  task = readTask(task.task_id);
472
747
  }
473
748
  catch (error) {
474
749
  if (error instanceof CoworkUnavailableError)
475
750
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
751
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
752
+ id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
753
+ eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
476
754
  throw error;
477
755
  }
478
756
  }
@@ -491,9 +769,23 @@ export class TaskRoomApplicationService {
491
769
  catch (error) {
492
770
  if (error instanceof CoworkUnavailableError)
493
771
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
772
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
773
+ id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
774
+ eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
494
775
  throw error;
495
776
  }
496
777
  }
778
+ const resultingRoom = getRoomRecord(task.room_id);
779
+ if (task.state === 'active' && resultingRoom?.state === 'active')
780
+ recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
781
+ title: task.title, previousState: 'provisioning', newState: task.state,
782
+ revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
783
+ list: task.list_name ?? 'default', roomId: task.room_id,
784
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
785
+ agents: taskAgents(task) });
786
+ else if (resultingRoom)
787
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
788
+ id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
497
789
  return { task, status: 'ready' };
498
790
  }
499
791
  createTemplate(cfg, requested, noRoom) {
@@ -507,17 +799,17 @@ export class TaskRoomApplicationService {
507
799
  throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
508
800
  return undefined;
509
801
  }
510
- return snapshotTemplate(definition);
802
+ return snapshotTemplate(definition, cfg.agentTemplates);
511
803
  }
512
804
  existingTemplate(cfg, task) {
513
805
  const ref = task.template;
514
806
  const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
515
- const snapshot = definition ? snapshotTemplate(definition) : undefined;
807
+ const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
516
808
  if (!snapshot || snapshot.content_hash !== ref.content_hash)
517
809
  throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
518
810
  return snapshot;
519
811
  }
520
- async provisionRoom(cfg, task, template, onCreated) {
812
+ async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
521
813
  const rooms = cfg.rooms;
522
814
  if (!rooms)
523
815
  throw new ConfigError('rooms: configuration is required');
@@ -529,17 +821,52 @@ export class TaskRoomApplicationService {
529
821
  throw new ConfigError('rooms: configuration is required before creating or querying rooms');
530
822
  return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
531
823
  })();
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);
824
+ const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
825
+ let launchTemplate;
826
+ let room;
827
+ try {
828
+ launchTemplate = template ? (template.launch_snapshot_hash ? template
829
+ : sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
830
+ // Room publication window: for a task-bound room, hold the common
831
+ // task-operation lock across remote creation, local record creation, and
832
+ // the task link, so an accepted deletion linearizes strictly before
833
+ // (bounded task_deleting abort) or strictly after (record visible to the
834
+ // deletion scan). Lock order stays launch-snapshot → task-operation.
835
+ const publishRoom = async () => {
836
+ if (task.task_id) {
837
+ const fresh = readTask(task.task_id);
838
+ if (fresh.deletion?.status === 'pending')
839
+ throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
840
+ }
841
+ const created = await cowork.createRoom({
842
+ room_name: task.title, goal: task.goal?.trim() || task.title,
843
+ briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
844
+ quiet_membership: launchTemplate?.room?.quiet_membership,
845
+ anonymous: policy.anonymous,
846
+ });
847
+ const record = createRoomRecord({
848
+ room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
849
+ task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
850
+ });
851
+ onCreated(record);
852
+ return record;
853
+ };
854
+ room = task.task_id
855
+ ? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
856
+ : await publishRoom();
857
+ }
858
+ catch (error) {
859
+ unlockSnapshot?.();
860
+ if (launchTemplate?.launch_snapshot_hash)
861
+ releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
862
+ throw error;
863
+ }
864
+ unlockSnapshot?.();
865
+ recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
866
+ name: room.room_name, previousState: 'none', newState: 'provisioning',
867
+ revision: room.created_at, taskId: room.task_id,
868
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
869
+ participants: [] });
543
870
  room = advanceSaga(room.room_id, 'create_room', 1);
544
871
  if (attachOwner) {
545
872
  try {
@@ -553,18 +880,39 @@ export class TaskRoomApplicationService {
553
880
  const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
554
881
  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
882
  : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
883
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
884
+ id: room.room_id, state: getRoomRecord(room.room_id)?.state ?? 'provisioning',
885
+ category: 'provision_failed', eventId: roomFailureEventId(getRoomRecord(room.room_id) ?? room) });
556
886
  throw error;
557
887
  }
558
888
  }
559
889
  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)
890
+ if (launchTemplate?.members.length)
891
+ try {
892
+ room = await (this.deps.provisionMembers ?? provisionMembers)({
893
+ cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
894
+ binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
895
+ goal: task.task_id ? task.title : task.goal,
896
+ });
897
+ }
898
+ catch (error) {
899
+ const failed = getRoomRecord(room.room_id) ?? room;
900
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: failed.room_id,
901
+ state: failed.state, category: 'provision_failed', eventId: roomFailureEventId(failed) });
902
+ throw error;
903
+ }
904
+ else
905
+ room = activateRoom(room.room_id);
906
+ if (room.state === 'active')
907
+ recordFleetAuditPresentation({ kind: 'room', operation: 'activate', id: room.room_id,
908
+ name: room.room_name, previousState: 'provisioning', newState: 'active',
909
+ revision: room.activated_at ?? room.created_at, taskId: room.task_id,
910
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
911
+ participants: roomParticipants(room) });
912
+ else
913
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
914
+ state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
915
+ if (task.task_id && !launchTemplate?.members.length)
568
916
  activateTask(task.task_id);
569
917
  return room;
570
918
  }