@ours.network/fleet 1.2.0-nightly.2 → 1.2.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 (277) hide show
  1. package/README.md +152 -29
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/correspondence.d.ts +10 -0
  7. package/dist/agent-ours/correspondence.js +1 -0
  8. package/dist/agent-ours/file-rpc.d.ts +23 -0
  9. package/dist/agent-ours/file-rpc.js +123 -0
  10. package/dist/agent-ours/harness.d.ts +14 -0
  11. package/dist/agent-ours/harness.js +72 -0
  12. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  13. package/dist/agent-ours/mcp-endpoint.js +111 -0
  14. package/dist/agent-ours/runtime.d.ts +59 -0
  15. package/dist/agent-ours/runtime.js +283 -0
  16. package/dist/agent-ours/service.d.ts +23 -0
  17. package/dist/agent-ours/service.js +372 -0
  18. package/dist/agent-ours/state.d.ts +45 -0
  19. package/dist/agent-ours/state.js +95 -0
  20. package/dist/agent-ours/wire.d.ts +14 -0
  21. package/dist/agent-ours/wire.js +55 -0
  22. package/dist/agent-recovery-gate.js +1 -1
  23. package/dist/application/conversation-history.d.ts +6 -0
  24. package/dist/application/conversation-history.js +65 -0
  25. package/dist/application/fleet-query-service.d.ts +1 -1
  26. package/dist/application/fleet-query-service.js +2 -2
  27. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  28. package/dist/application/legacy-supervisor-identity.js +43 -0
  29. package/dist/application/model-catalog.d.ts +1 -1
  30. package/dist/application/model-catalog.js +5 -2
  31. package/dist/application/preset-provenance.d.ts +19 -0
  32. package/dist/application/preset-provenance.js +59 -0
  33. package/dist/application/room-layout-definitions.d.ts +45 -0
  34. package/dist/application/room-layout-definitions.js +130 -0
  35. package/dist/application/session-control.d.ts +8 -0
  36. package/dist/application/session-control.js +21 -3
  37. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  38. package/dist/application/supervisor-ours-tools.js +113 -0
  39. package/dist/application/task-layouts.d.ts +89 -0
  40. package/dist/application/task-layouts.js +248 -0
  41. package/dist/application/task-room-service.d.ts +99 -3
  42. package/dist/application/task-room-service.js +296 -31
  43. package/dist/atomic-file.d.ts +5 -0
  44. package/dist/atomic-file.js +62 -1
  45. package/dist/briefing.js +10 -59
  46. package/dist/build-info.json +5 -4
  47. package/dist/capabilities.d.ts +2 -1
  48. package/dist/capabilities.js +2 -0
  49. package/dist/cli.js +65 -2
  50. package/dist/client-profile.d.ts +5 -16
  51. package/dist/client-profile.js +4 -112
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +7 -0
  54. package/dist/creation.d.ts +1 -1
  55. package/dist/creation.js +5 -16
  56. package/dist/daemon-recovery.d.ts +1 -8
  57. package/dist/daemon-recovery.js +8 -113
  58. package/dist/docs.d.ts +3 -3
  59. package/dist/docs.js +43 -23
  60. package/dist/doctor.d.ts +1 -1
  61. package/dist/doctor.js +46 -59
  62. package/dist/erased-resources.d.ts +9 -0
  63. package/dist/erased-resources.js +87 -0
  64. package/dist/fleet-command-audit.d.ts +1 -0
  65. package/dist/fleet-command-audit.js +12 -7
  66. package/dist/harness/agent-session.d.ts +2 -0
  67. package/dist/harness/claude-code-session.js +4 -2
  68. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  69. package/dist/harness/codex-app-server-proxy.js +41 -5
  70. package/dist/harness/codex-session.js +9 -2
  71. package/dist/harness/codex.d.ts +3 -2
  72. package/dist/harness/codex.js +4 -3
  73. package/dist/harness/hermes-session.js +7 -1
  74. package/dist/init-wizard.js +20 -9
  75. package/dist/monitor.d.ts +1 -34
  76. package/dist/monitor.js +24 -151
  77. package/dist/owner-channel/channel.d.ts +3 -0
  78. package/dist/owner-channel/channel.js +34 -6
  79. package/dist/owner-channel/commands.d.ts +4 -0
  80. package/dist/owner-channel/commands.js +8 -0
  81. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  82. package/dist/owner-channel/ours-client.js +2 -17
  83. package/dist/preset-migration.js +15 -0
  84. package/dist/rooms-tasks/add-member.d.ts +48 -0
  85. package/dist/rooms-tasks/add-member.js +83 -0
  86. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  87. package/dist/rooms-tasks/archived-absence.js +48 -0
  88. package/dist/rooms-tasks/cli.js +34 -5
  89. package/dist/rooms-tasks/close.d.ts +5 -2
  90. package/dist/rooms-tasks/close.js +208 -28
  91. package/dist/rooms-tasks/config.js +3 -1
  92. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  93. package/dist/rooms-tasks/cowork-adapter.js +11 -117
  94. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  95. package/dist/rooms-tasks/cowork-http.js +70 -0
  96. package/dist/rooms-tasks/deletion.d.ts +1 -1
  97. package/dist/rooms-tasks/deletion.js +74 -5
  98. package/dist/rooms-tasks/erasure.d.ts +6 -0
  99. package/dist/rooms-tasks/erasure.js +169 -0
  100. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  101. package/dist/rooms-tasks/external-worker.js +3 -4
  102. package/dist/rooms-tasks/layout-cli.d.ts +2 -0
  103. package/dist/rooms-tasks/layout-cli.js +81 -0
  104. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  105. package/dist/rooms-tasks/layout-config.js +89 -0
  106. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  107. package/dist/rooms-tasks/layout-control.js +52 -0
  108. package/dist/rooms-tasks/layout-service.d.ts +39 -0
  109. package/dist/rooms-tasks/layout-service.js +173 -0
  110. package/dist/rooms-tasks/layout.d.ts +102 -0
  111. package/dist/rooms-tasks/layout.js +227 -0
  112. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  113. package/dist/rooms-tasks/live-readiness.js +85 -0
  114. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  115. package/dist/rooms-tasks/member-startup.js +2 -0
  116. package/dist/rooms-tasks/provision.d.ts +14 -0
  117. package/dist/rooms-tasks/provision.js +182 -48
  118. package/dist/rooms-tasks/room-state.d.ts +4 -2
  119. package/dist/rooms-tasks/room-state.js +53 -8
  120. package/dist/rooms-tasks/task-state.d.ts +42 -7
  121. package/dist/rooms-tasks/task-state.js +153 -23
  122. package/dist/rooms-tasks/types.d.ts +28 -0
  123. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  124. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  125. package/dist/rooms-tasks/workspace.d.ts +26 -0
  126. package/dist/rooms-tasks/workspace.js +195 -0
  127. package/dist/runner.d.ts +23 -3
  128. package/dist/runner.js +574 -494
  129. package/dist/session/acp.d.ts +3 -0
  130. package/dist/session/acp.js +7 -0
  131. package/dist/session/arbiter.d.ts +3 -0
  132. package/dist/session/arbiter.js +20 -2
  133. package/dist/session/codex-app-server-transport.js +1 -1
  134. package/dist/session/codex-app-server.d.ts +3 -0
  135. package/dist/session/codex-app-server.js +25 -1
  136. package/dist/session/control.d.ts +10 -1
  137. package/dist/session/control.js +36 -2
  138. package/dist/session/conversation-normalizer.d.ts +2 -1
  139. package/dist/session/conversation-normalizer.js +1 -1
  140. package/dist/session/conversation-types.d.ts +5 -3
  141. package/dist/session/task-notice.d.ts +14 -0
  142. package/dist/session/task-notice.js +13 -0
  143. package/dist/session/types.d.ts +3 -0
  144. package/dist/socket-path.d.ts +2 -0
  145. package/dist/socket-path.js +22 -0
  146. package/dist/spawn.js +19 -16
  147. package/dist/startup-readiness.d.ts +13 -0
  148. package/dist/startup-readiness.js +38 -0
  149. package/dist/supervisor/launchd.js +2 -1
  150. package/dist/supervisor/systemd.js +9 -23
  151. package/dist/temp-idle.d.ts +17 -0
  152. package/dist/temp-idle.js +42 -0
  153. package/dist/temp-lifecycle.d.ts +6 -1
  154. package/dist/temp-lifecycle.js +29 -13
  155. package/dist/temp-supervisor-recovery.d.ts +3 -0
  156. package/dist/temp-supervisor-recovery.js +46 -0
  157. package/dist/watchdog/briefing.js +3 -16
  158. package/dist/watchdog/run.js +19 -5
  159. package/dist/web/agent-attachments.d.ts +29 -0
  160. package/dist/web/agent-attachments.js +162 -0
  161. package/dist/web/auth.d.ts +4 -1
  162. package/dist/web/auth.js +23 -2
  163. package/dist/web/control.js +2 -1
  164. package/dist/web/fleet-config-service.d.ts +4 -1
  165. package/dist/web/fleet-config-service.js +46 -15
  166. package/dist/web/prefix-gateway.d.ts +20 -0
  167. package/dist/web/prefix-gateway.js +150 -0
  168. package/dist/web/runtime.d.ts +4 -0
  169. package/dist/web/runtime.js +19 -14
  170. package/dist/web/server.d.ts +7 -0
  171. package/dist/web/server.js +293 -25
  172. package/docs/room-layouts.md +92 -0
  173. package/docs/task-workspaces.md +130 -0
  174. package/examples/room-layouts/fresh.yaml +27 -0
  175. package/examples/room-layouts/shared.yaml +19 -0
  176. package/package.json +17 -13
  177. package/presets/README.md +78 -0
  178. package/presets/brain-catalog.json +8 -2
  179. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  180. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  181. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  182. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  183. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  184. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  185. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  186. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  187. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  188. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  189. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  190. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  191. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  192. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  193. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  194. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  195. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  196. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  197. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  198. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  199. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  200. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  201. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  202. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  203. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  204. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  205. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  206. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  207. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  208. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  209. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  210. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  211. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  212. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  213. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  214. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  215. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  216. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  217. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  218. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  219. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  220. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  221. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  222. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  223. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  224. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  225. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  226. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  227. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  228. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  229. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  230. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  231. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  232. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  233. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  234. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  235. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  236. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  237. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  238. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  239. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  240. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  241. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  242. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  243. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  244. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  245. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  246. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  247. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  248. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  249. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  250. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  251. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  252. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  253. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  254. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  255. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  256. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  257. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  258. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  259. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  260. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  261. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  262. package/presets/fleet/roles/Coordinator.yaml +22 -0
  263. package/presets/fleet/roles/Engineer.yaml +40 -0
  264. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  265. package/presets/fleet/room_layouts/pair.yaml +24 -0
  266. package/presets/fleet/room_layouts/single.yaml +17 -0
  267. package/presets/fleet/room_layouts/team.yaml +30 -0
  268. package/presets/fleet/room_templates/engineering.yaml +19 -0
  269. package/presets/manifest.json +2 -2
  270. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  271. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  272. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  273. package/dist/web-app/icons/ours-fleet.svg +0 -4
  274. package/dist/web-app/index.html +0 -17
  275. package/dist/web-app/manifest.webmanifest +0 -15
  276. package/dist/web-app/offline.html +0 -18
  277. package/dist/web-app/sw.js +0 -51
@@ -1,3 +1,5 @@
1
+ import { readRoomReadiness } from '../agent-ours/service.js';
2
+ import { ensureWorkspace, validateWorkspace } from './workspace.js';
1
3
  import { createHash, randomUUID } from 'node:crypto';
2
4
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
5
  import { join } from 'node:path';
@@ -13,7 +15,7 @@ import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js
13
15
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
14
16
  import { controlRequest } from '../session/control.js';
15
17
  import { SessionControlError } from '../session/types.js';
16
- import { closeManagedRoom } from './close.js';
18
+ import { closeManagedRoom, roomCloseLockPath, CLOSE_LOCK_STALE_MS } from './close.js';
17
19
  import { withFileLock } from '../atomic-file.js';
18
20
  import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
19
21
  import { TaskStateError, taskDeletionState } from './task-state.js';
@@ -115,6 +117,7 @@ function settingsFor(member, cfg, sealed) {
115
117
  }
116
118
  function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
117
119
  return buildRoomMemberTask({
120
+ workspace: getRoomRecord(input.roomId)?.workspace?.path,
118
121
  taskId: input.taskId,
119
122
  roomId: input.roomId,
120
123
  roomIdentityCid,
@@ -128,7 +131,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
128
131
  cowork_role: member.coworkRole,
129
132
  persona: settings.persona,
130
133
  },
131
- roster: members.map(candidate => ({
134
+ roster: [...(input.append ? (getRoomRecord(input.roomId)?.member_seats ?? []).filter(seat => !members.some(m => m.name === seat.role_name)).map(seat => ({ name: seat.role_name, coworkRole: seat.cowork_role })) : []), ...members].map(candidate => ({
132
135
  role_name: candidate.name,
133
136
  cowork_role: candidate.coworkRole,
134
137
  })),
@@ -141,6 +144,10 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
141
144
  try {
142
145
  const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
143
146
  const startup = role.roomMemberStartup;
147
+ const workspace = getRoomRecord(roomId)?.workspace;
148
+ if (workspace && (role.cwd !== workspace.path
149
+ || JSON.stringify(startup?.workspace) !== JSON.stringify(workspace)))
150
+ return false;
144
151
  return role.identity === member.name
145
152
  && startup?.room_id === roomId
146
153
  && startup.room_identity_cid === roomIdentityCid
@@ -149,8 +156,7 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
149
156
  && (startup.anonymous ?? false) === anonymous
150
157
  && sha256Text(startup.task ?? '') === taskSha
151
158
  && (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
152
- && typeof startup.invite === 'string'
153
- && startup.invite.length > 0;
159
+ && startup.invite === '';
154
160
  }
155
161
  catch {
156
162
  return false;
@@ -234,25 +240,9 @@ async function retainRunningLaunch(input) {
234
240
  }
235
241
  return false;
236
242
  }
237
- /**
238
- * Member-launch publication window: for a task-bound room, the durable launch
239
- * intent, spawn, and launch record publish under the common task-operation
240
- * lock so an accepted deletion linearizes strictly before (bounded abort — no
241
- * spawn happens) or strictly after (the seat's launch record is visible to
242
- * deletion retirement). The lock is held per member only, never across
243
- * seat-wait loops.
244
- */
245
243
  async function launchMember(input) {
246
- const taskId = input.provision.taskId;
247
- if (!taskId)
248
- return launchMemberUnlocked(input);
249
- return withFileLock(taskOperationLockPath(taskId), () => {
250
- if (taskDeletionState(taskId) !== 'none')
251
- throw new Error(`task ${taskId} is pending deletion; aborting member launch for ${input.member.name}`);
252
- return launchMemberUnlocked(input);
253
- }, {}, TASK_OPERATION_LOCK_STALE_MS);
254
- }
255
- async function launchMemberUnlocked(input) {
244
+ // provisionMembers holds the task and room lifecycle locks through every launch.
245
+ assertProvisioningOpen(input.provision);
256
246
  const { provision, member, settings, startup } = input;
257
247
  const seat = getRoomRecord(provision.roomId).member_seats
258
248
  .find(candidate => candidate.role_name === member.name);
@@ -274,6 +264,7 @@ async function launchMemberUnlocked(input) {
274
264
  try {
275
265
  const launched = await spawnRoomMember({
276
266
  name: member.name,
267
+ configPath: provision.configPath,
277
268
  temp: true,
278
269
  identity: member.name,
279
270
  agentDefinition: settings.definition,
@@ -345,7 +336,16 @@ function reconcileMemberSeats(roomId, members, observed, ownerSeatCid) {
345
336
  && seat.seat_state === 'active');
346
337
  const seats = current.member_seats.map(seat => {
347
338
  const member = members.find(candidate => candidate.name === seat.role_name);
339
+ if (!member)
340
+ return seat;
341
+ const readiness = readRoomReadiness(current.room_identity_cid, member.name);
342
+ if (!readiness || readiness.room !== roomId || readiness.invite !== seat.invite_id) {
343
+ complete = false;
344
+ return seat;
345
+ }
348
346
  const found = exactCoworkSeat(observed, member, seat.invite_id);
347
+ if (found && found.identity_cid !== readiness.cid)
348
+ throw new Error('Cowork seat CID does not match supervisor provisioned identity');
349
349
  if (!found || found.seat_state !== 'active') {
350
350
  complete = false;
351
351
  return seat;
@@ -364,9 +364,41 @@ function assertCoworkRoomPolicy(room, expectedAnonymous) {
364
364
  if ((room.anonymous ?? false) !== expectedAnonymous)
365
365
  throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
366
366
  }
367
+ function assertProvisioningOpen(input) {
368
+ if (input.taskId && getTask(input.taskId).terminal_intent)
369
+ throw new Error(`task ${input.taskId} has an accepted terminal intent; refusing to provision members`);
370
+ const room = getRoomRecord(input.roomId);
371
+ if (room?.state === 'closing' || room?.state === 'closed')
372
+ throw new Error(`room ${input.roomId} is ${room.state}; refusing to provision members`);
373
+ }
367
374
  export async function provisionMembers(input) {
375
+ // Same lock order as terminal/deletion settlement. Serialize concurrent retries
376
+ // and standalone retirement as well as member publication. Seat waits are bounded.
377
+ const run = () => withFileLock(roomCloseLockPath(input.roomId), () => {
378
+ assertProvisioningOpen(input);
379
+ if (input.append) {
380
+ const task = input.taskId ? getTask(input.taskId) : undefined;
381
+ const room = getRoomRecord(input.roomId);
382
+ if (!task || !['active', 'review'].includes(task.state) || task.blocked || room?.state !== 'active'
383
+ || room.task_id !== task.task_id || room.room_identity_cid !== task.room_identity_cid)
384
+ throw new Error('Adding an agent requires the exact active, unblocked task room');
385
+ const names = expandMembers(input.template, shortId(task.task_id)).map(m => m.name);
386
+ if (room.member_seats.some(seat => names.includes(seat.role_name)))
387
+ throw new Error('Member slot is already recorded; inspect its existing launch');
388
+ if (room.member_seats.some(seat => seat.seat_state !== 'active'))
389
+ throw new Error('Existing member admission must finish before adding an agent');
390
+ }
391
+ if (!getRoomRecord(input.roomId))
392
+ throw new Error(`room ${input.roomId} is missing; refusing provisioning`);
393
+ return provisionMembersUnlocked(input);
394
+ }, {}, CLOSE_LOCK_STALE_MS);
395
+ return input.taskId
396
+ ? withFileLock(taskOperationLockPath(input.taskId), run, {}, TASK_OPERATION_LOCK_STALE_MS)
397
+ : run();
398
+ }
399
+ async function provisionMembersUnlocked(input) {
368
400
  const { cfg, cowork, roomId, taskId, template } = input;
369
- // Deletion-epoch pre-check; each member launch re-checks under the lock.
401
+ // The common task-operation lock protects this epoch through all member launches.
370
402
  if (taskId && taskDeletionState(taskId) !== 'none')
371
403
  throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
372
404
  const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
@@ -378,6 +410,12 @@ export async function provisionMembers(input) {
378
410
  const existing = getRoomRecord(roomId);
379
411
  if (!existing?.room_identity_cid)
380
412
  throw new Error(`room ${roomId} has no pinned room identity CID`);
413
+ if (existing.workspace) {
414
+ validateWorkspace(existing.workspace, taskId ? 'task' : 'room', taskId ?? roomId);
415
+ ensureWorkspace(existing.workspace);
416
+ for (const setting of settings.values())
417
+ setting.definition.cwd = existing.workspace.path;
418
+ }
381
419
  const roomIdentityCid = existing.room_identity_cid;
382
420
  const ownerSeatCid = existing.owner_seat_cid ?? null;
383
421
  const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
@@ -385,22 +423,23 @@ export async function provisionMembers(input) {
385
423
  const resuming = members.length > 0
386
424
  && members.every(member => persistedNames.has(member.name));
387
425
  if (!resuming) {
388
- advanceSaga(roomId, 'create_members', 3);
389
- updateMemberSeats(roomId, members.map(member => {
390
- const memberSettings = settings.get(member.name);
391
- const evidence = launchDefinition(memberSettings.definition);
392
- return ({
393
- role_name: member.name,
394
- slot: member.slot,
395
- cowork_role: member.coworkRole,
396
- seat_state: 'pending',
397
- launch: { state: 'pending', attempt: 0,
398
- agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
399
- agent_template: memberSettings.template,
400
- agent_template_hash: memberSettings.templateHash,
401
- updated_at: new Date().toISOString() },
402
- });
403
- }));
426
+ if (!input.append)
427
+ advanceSaga(roomId, 'create_members', 3);
428
+ updateMemberSeats(roomId, [...(input.append ? existing.member_seats : []), ...members.map(member => {
429
+ const memberSettings = settings.get(member.name);
430
+ const evidence = launchDefinition(memberSettings.definition);
431
+ return ({
432
+ role_name: member.name,
433
+ slot: member.slot,
434
+ cowork_role: member.coworkRole,
435
+ seat_state: 'pending',
436
+ launch: { state: 'pending', attempt: 0,
437
+ agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
438
+ agent_template: memberSettings.template,
439
+ agent_template_hash: memberSettings.templateHash,
440
+ updated_at: new Date().toISOString() },
441
+ });
442
+ })], input.append ? template : undefined);
404
443
  }
405
444
  else {
406
445
  for (const member of members) {
@@ -418,7 +457,8 @@ export async function provisionMembers(input) {
418
457
  now: input.startupWait?.now ?? Date.now,
419
458
  sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
420
459
  };
421
- advanceSaga(roomId, 'join_role_groups', 4);
460
+ if (!input.append)
461
+ advanceSaga(roomId, 'join_role_groups', 4);
422
462
  try {
423
463
  const initialRoom = await cowork.recoverRoom(roomId);
424
464
  assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
@@ -439,17 +479,31 @@ export async function provisionMembers(input) {
439
479
  provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
440
480
  }))
441
481
  continue;
482
+ // A previous failed launch keeps its invite pointer durably. Never
483
+ // overwrite that requirement until the supported revoke has succeeded;
484
+ // a transport failure must fence subsequent invite issuance too.
485
+ if (currentSeat.invite_id) {
486
+ const observed = await cowork.getRoom(roomId);
487
+ if (!observed || observed.identity_cid !== roomIdentityCid)
488
+ throw new Error('cannot verify room before failed-attempt invite cleanup');
489
+ if (observed.seats.some(seat => seat.invite_id === currentSeat.invite_id
490
+ && seat.seat_state !== 'removed'))
491
+ throw new Error('failed-attempt invite has an admitted seat; reconcile before replacing the member');
492
+ await cowork.revokeInvite(roomId, currentSeat.invite_id);
493
+ }
442
494
  const issued = await cowork.issueInvite(roomId, {
443
495
  mode: 'one_time', role: member.coworkRole, min_accepts: 1,
444
496
  });
445
- const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
446
- updateMemberSeats(roomId, seats);
447
497
  try {
498
+ assertProvisioningOpen(input);
499
+ const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
500
+ updateMemberSeats(roomId, seats);
448
501
  await launchMember({
449
502
  provision: input,
450
503
  member,
451
504
  settings: settings.get(member.name),
452
505
  startup: {
506
+ workspace: existing.workspace,
453
507
  room_id: roomId,
454
508
  room_identity_cid: roomIdentityCid,
455
509
  identity_name: member.name,
@@ -463,7 +517,12 @@ export async function provisionMembers(input) {
463
517
  });
464
518
  }
465
519
  catch (error) {
466
- await cowork.revokeInvite(roomId, issued.invite_id).catch(() => { });
520
+ try {
521
+ await cowork.revokeInvite(roomId, issued.invite_id);
522
+ }
523
+ catch (cleanupError) {
524
+ throw new AggregateError([error, cleanupError], 'member launch failed and invite cleanup is unresolved; retry must revoke the retained requirement first');
525
+ }
467
526
  throw error;
468
527
  }
469
528
  }
@@ -484,20 +543,23 @@ export async function provisionMembers(input) {
484
543
  }
485
544
  throw error;
486
545
  }
487
- advanceSaga(roomId, 'wait_seats', 5);
546
+ if (!input.append)
547
+ advanceSaga(roomId, 'wait_seats', 5);
488
548
  const deadline = policy.now() + policy.timeoutMs;
489
549
  let delay = policy.initialDelayMs;
490
550
  for (;;) {
491
551
  const remote = await cowork.recoverRoom(roomId);
492
552
  assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
493
553
  const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
494
- if (reconciled.complete)
554
+ if (reconciled.complete && remote.state === 'active')
495
555
  break;
496
556
  if (policy.now() >= deadline) {
497
- advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
557
+ if (!input.append)
558
+ advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
498
559
  return getRoomRecord(roomId);
499
560
  }
500
- advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
561
+ if (!input.append)
562
+ advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
501
563
  await policy.sleep(delay);
502
564
  delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
503
565
  }
@@ -522,15 +584,87 @@ export async function provisionMembers(input) {
522
584
  };
523
585
  });
524
586
  updateTaskMembers(taskId, taskMembers);
525
- if (getTask(taskId).blocked)
587
+ if (!input.append && getTask(taskId).blocked)
526
588
  unblockTask(taskId);
527
589
  }
590
+ if (input.append)
591
+ return getRoomRecord(roomId);
528
592
  advanceSaga(roomId, 'activate', 6);
529
593
  const record = activateRoom(roomId);
530
594
  if (taskId)
531
595
  activateTask(taskId);
532
596
  return record;
533
597
  }
598
+ /** Reconcile proven, admitted launches without any spawn, archive, invite or recovery path. */
599
+ export async function reconcileExistingTaskMembers(input) {
600
+ return withFileLock(taskOperationLockPath(input.taskId), async () => {
601
+ const task = getTask(input.taskId);
602
+ if (!['provisioning', 'active'].includes(task.state) || task.terminal_intent || taskDeletionState(input.taskId) !== 'none'
603
+ || !task.room_id)
604
+ throw new Error('task is not open for existing-member reconciliation');
605
+ const roomId = task.room_id;
606
+ return withFileLock(roomCloseLockPath(roomId), async () => {
607
+ const current = getRoomRecord(roomId);
608
+ if (!current || !['provisioning', 'active'].includes(current.state) || current.close
609
+ || current.task_id !== input.taskId || !current.room_identity_cid
610
+ || task.room_identity_cid !== current.room_identity_cid)
611
+ throw new Error('room is not open for existing-member reconciliation');
612
+ const names = new Set(input.expectedLaunches.map(seat => seat.role_name));
613
+ if (!names.size || names.size !== input.expectedLaunches.length
614
+ || current.member_seats.length !== names.size
615
+ || current.member_seats.some(seat => !names.has(seat.role_name)))
616
+ throw new Error('expected launch roster does not match the persisted room');
617
+ const remote = await input.cowork.getRoom(roomId);
618
+ if (!remote || remote.room_id !== roomId || remote.identity_cid !== current.room_identity_cid
619
+ || remote.state !== 'active')
620
+ throw new Error('Cowork room is not the exact active room');
621
+ assertCoworkRoomPolicy(remote, storedRoomLaunchPolicy(current.room_policy).anonymous);
622
+ if (current.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === current.owner_seat_cid && seat.seat_state === 'active'))
623
+ throw new Error('expected Owner seat is not active');
624
+ const seats = [];
625
+ for (const seat of current.member_seats) {
626
+ const expected = input.expectedLaunches.find(item => item.role_name === seat.role_name);
627
+ const launch = seat.launch;
628
+ if (seat.seat_state === 'removed' || !seat.invite_id || launch?.state !== 'launched'
629
+ || launch.launch_id !== expected.launch_id || !launch.action_id || !launch.mission_sha256
630
+ || (seat.identity_cid && seat.identity_cid !== expected.identity_cid))
631
+ throw new Error('persisted member launch does not match reconciliation evidence');
632
+ const dir = agentDir(seat.role_name, true);
633
+ if (!launchMatches(dir, { name: seat.role_name, coworkRole: seat.cowork_role }, launch.action_id, launch.mission_sha256, roomId, current.room_identity_cid, seat.invite_id, storedRoomLaunchPolicy(current.room_policy).anonymous))
634
+ throw new Error('member startup provenance mismatch');
635
+ const supervisor = readTempSupervisor(dir);
636
+ if (!supervisor || supervisor.role !== seat.role_name || supervisor.launchId !== expected.launch_id
637
+ || await tempSupervisorLiveness(dir) !== 'running')
638
+ throw new Error('expected member supervisor is not running');
639
+ const ready = readRoomReadiness(current.room_identity_cid, seat.role_name);
640
+ const matches = remote.seats.filter(item => item.display_name === seat.role_name
641
+ && item.seat_state !== 'removed');
642
+ const found = matches[0];
643
+ if (!ready || ready.room !== roomId || ready.invite !== seat.invite_id
644
+ || ready.cid !== expected.identity_cid || matches.length !== 1 || !found
645
+ || found.identity_cid !== expected.identity_cid || found.role !== seat.cowork_role
646
+ || found.seat_state !== 'active' || found.invite_id !== seat.invite_id)
647
+ throw new Error('member readiness or authenticated Cowork seat mismatch');
648
+ seats.push({ ...seat, identity_cid: expected.identity_cid, seat_state: 'active' });
649
+ }
650
+ if (input.checkOnly)
651
+ return current;
652
+ // No mutation until the complete roster has passed; these writes are replayable
653
+ // under the same task/room lifecycle locks if interrupted between files.
654
+ updateMemberSeats(roomId, seats);
655
+ updateTaskMembers(input.taskId, seats.map(seat => ({ name: seat.role_name,
656
+ identity_cid: seat.identity_cid, slot: seat.slot, cowork_role: seat.cowork_role })));
657
+ if (getTask(input.taskId).blocked)
658
+ unblockTask(input.taskId);
659
+ if (current.state !== 'active')
660
+ advanceSaga(roomId, 'activate', 6);
661
+ const activated = current.state === 'active' ? getRoomRecord(roomId) : activateRoom(roomId);
662
+ if (task.state !== 'active')
663
+ activateTask(input.taskId);
664
+ return activated;
665
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
666
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
667
+ }
534
668
  export async function cleanupMembers(input) {
535
669
  const room = getRoomRecord(input.roomId);
536
670
  if (!room)
@@ -22,7 +22,7 @@ export declare function advanceSaga(id: string, phase: SagaPhase, stepIndex?: nu
22
22
  export declare function setSagaError(id: string, error: string, hint?: string, detail?: ProvisioningDetail): RoomOrchestrationRecord;
23
23
  export declare function setRoomIdentity(id: string, roomIdentityCid: string): RoomOrchestrationRecord;
24
24
  export declare function setOwnerSeat(id: string, ownerSeatCid: string, inviteFingerprint: string): RoomOrchestrationRecord;
25
- export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[]): RoomOrchestrationRecord;
25
+ export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[], additionalTemplate?: import('./types.js').TemplateSnapshot): RoomOrchestrationRecord;
26
26
  export declare function updateRoomRoleBriefing(id: string, role: string, definition: RoomRoleBriefingDefinition): RoomOrchestrationRecord;
27
27
  export declare function updateRoomHistoryCursor(id: string, cursor: number): RoomOrchestrationRecord;
28
28
  export declare function updateMemberStartup(id: string, roleName: string, update: {
@@ -31,6 +31,8 @@ export declare function updateMemberStartup(id: string, roleName: string, update
31
31
  export declare function activateRoom(id: string): RoomOrchestrationRecord;
32
32
  export declare function closeRoom(id: string): RoomOrchestrationRecord;
33
33
  export declare function beginRoomClose(id: string): RoomOrchestrationRecord;
34
- export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string): RoomOrchestrationRecord;
34
+ export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string, absenceVerified?: boolean): RoomOrchestrationRecord;
35
35
  export declare function advanceRoomClose(id: string, phase: RoomClosePhase): RoomOrchestrationRecord;
36
36
  export declare function setRoomCloseError(id: string, error: string, recoveryHint: string): RoomOrchestrationRecord;
37
+ /** Settlement-only binding recovery; never publishes a seat or changes a launch. */
38
+ export declare function recordRetirementMemberCids(id: string, recovered: ReadonlyArray<RoomMemberSeat>): RoomOrchestrationRecord;
@@ -2,17 +2,28 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:f
2
2
  import { join } from 'node:path';
3
3
  import { replaceFileAtomically } from '../atomic-file.js';
4
4
  import { stateRoot } from '../paths.js';
5
+ import { planWorkspace, ensureWorkspace } from './workspace.js';
6
+ import { getTask } from './task-state.js';
5
7
  import { storedRoomLaunchPolicy } from './types.js';
6
8
  import { releaseLaunchSnapshot } from './launch-snapshot.js';
7
9
  export const roomsDir = () => join(stateRoot(), 'rooms');
8
- function roomPath(id) { return join(roomsDir(), `${id}.json`); }
10
+ function roomPath(id) {
11
+ if (!/^[a-zA-Z0-9_-]{1,120}$/.test(id))
12
+ throw new RoomStateError('Invalid room ID');
13
+ return join(roomsDir(), `${id}.json`);
14
+ }
9
15
  export class RoomStateError extends Error {
10
16
  }
11
17
  function readRoom(id) {
12
18
  const p = roomPath(id);
13
- if (!existsSync(p))
14
- throw new RoomStateError(`room not found: ${id}`);
15
- return JSON.parse(readFileSync(p, 'utf8'));
19
+ try {
20
+ return JSON.parse(readFileSync(p, 'utf8'));
21
+ }
22
+ catch (error) {
23
+ if (error.code === 'ENOENT')
24
+ throw new RoomStateError(`room not found: ${id}`);
25
+ throw error;
26
+ }
16
27
  }
17
28
  function writeRoom(record) {
18
29
  mkdirSync(roomsDir(), { recursive: true });
@@ -25,6 +36,8 @@ export function createRoomRecord(input) {
25
36
  const requested = storedRoomLaunchPolicy(input.room_policy);
26
37
  if (JSON.stringify(before) !== JSON.stringify(requested))
27
38
  throw new RoomStateError(`room ${input.room_id} launch policy mismatch`);
39
+ if (existing.workspace)
40
+ ensureWorkspace(existing.workspace);
28
41
  return existing;
29
42
  }
30
43
  const record = {
@@ -40,15 +53,20 @@ export function createRoomRecord(input) {
40
53
  state: 'provisioning',
41
54
  created_at: new Date().toISOString(),
42
55
  };
56
+ record.workspace = input.task_id ? getTask(input.task_id).workspace : planWorkspace('room', input.room_id);
43
57
  writeRoom(record);
58
+ if (record.workspace)
59
+ ensureWorkspace(record.workspace);
44
60
  return record;
45
61
  }
46
62
  export function getRoomRecord(id) {
47
63
  try {
48
64
  return readRoom(id);
49
65
  }
50
- catch {
51
- return undefined;
66
+ catch (error) {
67
+ if (error instanceof RoomStateError && error.message === `room not found: ${id}`)
68
+ return undefined;
69
+ throw error;
52
70
  }
53
71
  }
54
72
  /** Remove a terminal orchestration record from the live room inventory. */
@@ -110,9 +128,13 @@ export function setOwnerSeat(id, ownerSeatCid, inviteFingerprint) {
110
128
  writeRoom(r);
111
129
  return r;
112
130
  }
113
- export function updateMemberSeats(id, seats) {
131
+ export function updateMemberSeats(id, seats, additionalTemplate) {
114
132
  const r = readRoom(id);
133
+ if (r.state === 'closing' || r.state === 'closed')
134
+ throw new RoomStateError(`room ${id} is ${r.state}; member provisioning is fenced`);
115
135
  r.member_seats = seats;
136
+ if (additionalTemplate)
137
+ r.additional_member_templates = [...(r.additional_member_templates ?? []), additionalTemplate];
116
138
  writeRoom(r);
117
139
  return r;
118
140
  }
@@ -135,6 +157,8 @@ const LAUNCH_ORDER = [
135
157
  ];
136
158
  export function updateMemberStartup(id, roleName, update) {
137
159
  const r = readRoom(id);
160
+ if (r.state === 'closing' || r.state === 'closed')
161
+ throw new RoomStateError(`room ${id} is ${r.state}; member provisioning is fenced`);
138
162
  const seat = r.member_seats.find(candidate => candidate.role_name === roleName);
139
163
  if (!seat)
140
164
  throw new RoomStateError(`room ${id} has no recorded member ${roleName}`);
@@ -151,6 +175,8 @@ export function updateMemberStartup(id, roleName, update) {
151
175
  }
152
176
  export function activateRoom(id) {
153
177
  const r = readRoom(id);
178
+ if (r.state === 'closing' || r.state === 'closed')
179
+ throw new RoomStateError(`room ${id} is ${r.state}; activation is fenced`);
154
180
  r.state = 'active';
155
181
  r.saga = { phase: 'completed', step_index: 0 };
156
182
  delete r.provisioning_detail;
@@ -198,7 +224,7 @@ export function beginRoomClose(id) {
198
224
  const RETIREMENT_ORDER = [
199
225
  'stop_requested', 'liveness_absent', 'archive_secured', 'identity_absent',
200
226
  ];
201
- export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath) {
227
+ export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath, absenceVerified = false) {
202
228
  const r = readRoom(id);
203
229
  if (r.state !== 'closing')
204
230
  throw new RoomStateError(`room ${id} is not closing`);
@@ -215,6 +241,7 @@ export function advanceMemberRetirement(id, roleName, phase, launchId, archivePa
215
241
  throw new RoomStateError(`room ${id} member ${roleName} retirement cannot move backward to ${phase}`);
216
242
  }
217
243
  seat.retirement = {
244
+ ...(absenceVerified || previous?.absence_verified ? { absence_verified: true } : {}),
218
245
  phase,
219
246
  launch_id: launchId,
220
247
  updated_at: new Date().toISOString(),
@@ -267,3 +294,21 @@ export function setRoomCloseError(id, error, recoveryHint) {
267
294
  writeRoom(r);
268
295
  return r;
269
296
  }
297
+ /** Settlement-only binding recovery; never publishes a seat or changes a launch. */
298
+ export function recordRetirementMemberCids(id, recovered) {
299
+ const room = readRoom(id);
300
+ if (room.state !== 'closing')
301
+ throw new RoomStateError('Room is not closing');
302
+ for (const proof of recovered) {
303
+ const seat = room.member_seats.find(value => value.role_name === proof.role_name);
304
+ if (!seat || seat.launch?.action_id !== proof.launch?.action_id || seat.invite_id !== proof.invite_id)
305
+ throw new RoomStateError('Member changed during identity recovery');
306
+ if (!proof.identity_cid)
307
+ continue;
308
+ if (seat.identity_cid && seat.identity_cid.toLowerCase() !== proof.identity_cid.toLowerCase())
309
+ throw new RoomStateError('Member identity changed during recovery');
310
+ seat.identity_cid = proof.identity_cid;
311
+ }
312
+ writeRoom(room);
313
+ return room;
314
+ }
@@ -1,3 +1,4 @@
1
+ import type { ArchivedMemberAbsence } from './types.js';
1
2
  import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
2
3
  export declare const tasksDir: () => string;
3
4
  export declare class TaskStateError extends Error {
@@ -17,6 +18,10 @@ export interface CreateTaskInput {
17
18
  idempotency_key?: string;
18
19
  start?: boolean;
19
20
  no_room?: boolean;
21
+ layout?: {
22
+ name: string;
23
+ definition_hash: string;
24
+ };
20
25
  room_id?: string;
21
26
  listId?: string;
22
27
  }
@@ -61,9 +66,8 @@ export declare function getDeletingTask(id: string): TaskRecord;
61
66
  export declare function taskDeletionState(id: string): 'none' | 'pending' | 'absent';
62
67
  /**
63
68
  * Durable, surface-independent audit evidence for a permanent deletion. The
64
- * receipt outlives the task record: written with the acceptance intent, and
65
- * completed before settlement is reported. Metadata only — never brief or
66
- * room content.
69
+ * receipt supports in-flight recovery only: written with acceptance intent,
70
+ * then erased before settlement is reported. It may contain a bounded title.
67
71
  */
68
72
  export interface TaskDeletionReceipt {
69
73
  schema_version: 1;
@@ -74,6 +78,7 @@ export interface TaskDeletionReceipt {
74
78
  original_state: TaskState;
75
79
  room_id?: string;
76
80
  member_count: number;
81
+ archived_absences?: ArchivedMemberAbsence[];
77
82
  settled_at?: string;
78
83
  result?: 'deleted';
79
84
  }
@@ -86,7 +91,7 @@ export declare function readTaskDeletionReceipt(id: string): TaskDeletionReceipt
86
91
  * settlement rather than deleting resources without audit evidence.
87
92
  */
88
93
  export declare function ensureTaskDeletionReceipt(id: string): void;
89
- /** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
94
+ /** Erase the in-flight receipt; also heals a crash after task unlink. */
90
95
  export declare function completeTaskDeletionReceipt(id: string): void;
91
96
  export type TaskDeletionAcceptance = {
92
97
  status: 'accepted' | 'pending';
@@ -122,7 +127,12 @@ export declare function advanceTaskDeletionMember(id: string, name: string, phas
122
127
  interface SeatEvidence {
123
128
  role_name: string;
124
129
  identity_cid?: string;
130
+ launch?: {
131
+ launch_id?: string;
132
+ action_id?: string;
133
+ };
125
134
  retirement?: {
135
+ absence_verified?: boolean;
126
136
  phase: TaskDeletionMemberPhase;
127
137
  launch_id: string;
128
138
  archive_path?: string;
@@ -146,14 +156,39 @@ export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: Re
146
156
  *
147
157
  * The room saga is trusted for phase jumps, but partial or corrupt retained
148
158
  * records must not become false success: every seat must be identity_absent;
149
- * a real launch requires archive evidence; an identity-less seat is accepted
150
- * only via the never-launched proof.
159
+ * a real launch requires archive evidence; an identity-less seat requires
160
+ * never-launched proof or a freshly verified archived-absence checkpoint.
151
161
  */
152
- export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>): TaskRecord;
162
+ export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>, proofs?: ReadonlyArray<ArchivedMemberAbsence>): TaskRecord;
163
+ /** Settlement-only checkpoint, under the task-operation lock after fresh archive verification.
164
+ * From here retries still verify live absence, but owned archives may be consumed.
165
+ */
166
+ export declare function beginTaskWorkspaceCleanup(id: string): void;
153
167
  /**
154
168
  * Physically remove a deletion-pending task record. Settlement-only: callers
155
169
  * must have completed member retirement and room cleanup first. Missing
156
170
  * records are the idempotent already-settled outcome.
157
171
  */
158
172
  export declare function unlinkDeletedTask(id: string): boolean;
173
+ /** Called only after current temp and exact identity absence have been checked. */
174
+ export declare function settleAbsentTaskDeletionMember(id: string, name: string): void;
175
+ /** Room-only erasure keeps the parent Task but removes its dead room/member links. */
176
+ export declare function detachDeletedRoom(taskId: string, roomId: string): void;
177
+ /** A completed room may consume its archives while other task members still retire. */
178
+ export declare function beginTaskArchiveCleanup(id: string): void;
179
+ /** Metadata-only edit; compare under the task lock to prevent lost updates. */
180
+ /**
181
+ * Choose, change or clear a Backlog task's Room Layout before it starts.
182
+ * `expected` is the layout name the caller saw (null = none); a different
183
+ * current choice is a stale edit. Choosing a layout drops the sealed room
184
+ * template plan; the caller releases the returned launch snapshot hash.
185
+ */
186
+ export declare function updateTaskLayout(id: string, expected: string | null, next: {
187
+ name: string;
188
+ definition_hash: string;
189
+ } | null): {
190
+ task: TaskRecord;
191
+ releasedSnapshot?: string;
192
+ };
193
+ export declare function updateTaskBrief(id: string, brief: string, expectedBrief: string): TaskRecord;
159
194
  export {};