@ours.network/fleet 1.2.0-nightly.5 → 1.2.0-nightly.50

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 (330) hide show
  1. package/README.md +263 -53
  2. package/dist/account-origin.d.ts +2 -0
  3. package/dist/account-origin.js +6 -0
  4. package/dist/agent-ours/bridge.js +9 -0
  5. package/dist/agent-ours/correspondence.d.ts +10 -0
  6. package/dist/agent-ours/correspondence.js +1 -0
  7. package/dist/agent-ours/harness.js +8 -0
  8. package/dist/agent-ours/runtime.d.ts +6 -0
  9. package/dist/agent-ours/runtime.js +36 -0
  10. package/dist/agent-ours/service.js +33 -13
  11. package/dist/application/codex-models.d.ts +15 -0
  12. package/dist/application/codex-models.js +56 -0
  13. package/dist/application/conversation-history.d.ts +25 -0
  14. package/dist/application/conversation-history.js +189 -0
  15. package/dist/application/fleet-query-service.d.ts +1 -1
  16. package/dist/application/fleet-query-service.js +2 -2
  17. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  18. package/dist/application/legacy-supervisor-identity.js +43 -0
  19. package/dist/application/model-catalog.d.ts +1 -1
  20. package/dist/application/model-catalog.js +5 -2
  21. package/dist/application/preset-provenance.d.ts +19 -0
  22. package/dist/application/preset-provenance.js +59 -0
  23. package/dist/application/role-creation-service.js +1 -1
  24. package/dist/application/role-removal-service.js +2 -0
  25. package/dist/application/role-repository.js +2 -2
  26. package/dist/application/room-layout-definitions.d.ts +45 -0
  27. package/dist/application/room-layout-definitions.js +130 -0
  28. package/dist/application/session-control.d.ts +30 -0
  29. package/dist/application/session-control.js +72 -3
  30. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  31. package/dist/application/supervisor-ours-tools.js +113 -0
  32. package/dist/application/task-layouts.d.ts +91 -0
  33. package/dist/application/task-layouts.js +249 -0
  34. package/dist/application/task-room-service.d.ts +99 -3
  35. package/dist/application/task-room-service.js +296 -31
  36. package/dist/atomic-file.d.ts +5 -0
  37. package/dist/atomic-file.js +62 -1
  38. package/dist/briefing.d.ts +2 -0
  39. package/dist/briefing.js +8 -1
  40. package/dist/build-info.json +7 -4
  41. package/dist/capabilities.d.ts +6 -1
  42. package/dist/capabilities.js +8 -0
  43. package/dist/cli.js +376 -34
  44. package/dist/client-profile.d.ts +5 -17
  45. package/dist/client-profile.js +4 -127
  46. package/dist/config.d.ts +28 -0
  47. package/dist/config.js +41 -6
  48. package/dist/creation.js +2 -2
  49. package/dist/daemon-recovery.d.ts +1 -8
  50. package/dist/daemon-recovery.js +8 -113
  51. package/dist/docs.d.ts +3 -3
  52. package/dist/docs.js +126 -22
  53. package/dist/doctor.d.ts +1 -1
  54. package/dist/doctor.js +55 -60
  55. package/dist/erased-resources.d.ts +9 -0
  56. package/dist/erased-resources.js +87 -0
  57. package/dist/fleet-command-audit.d.ts +7 -0
  58. package/dist/fleet-command-audit.js +26 -9
  59. package/dist/harness/claude-code.js +9 -1
  60. package/dist/harness/claude-runtime.d.ts +12 -0
  61. package/dist/harness/claude-runtime.js +43 -0
  62. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  63. package/dist/harness/codex-app-server-proxy.js +41 -5
  64. package/dist/harness/codex-reasoning.d.ts +5 -0
  65. package/dist/harness/codex-reasoning.js +12 -0
  66. package/dist/harness/codex-runtime.d.ts +11 -1
  67. package/dist/harness/codex-runtime.js +31 -4
  68. package/dist/harness/codex.d.ts +3 -2
  69. package/dist/harness/codex.js +20 -11
  70. package/dist/init-wizard.d.ts +2 -0
  71. package/dist/init-wizard.js +38 -19
  72. package/dist/isolation/policy.js +2 -0
  73. package/dist/isolation/types.d.ts +5 -0
  74. package/dist/managed-cli.d.ts +253 -0
  75. package/dist/managed-cli.js +923 -0
  76. package/dist/minimal-setup.d.ts +2 -0
  77. package/dist/minimal-setup.js +44 -0
  78. package/dist/monitor.d.ts +1 -34
  79. package/dist/monitor.js +24 -151
  80. package/dist/notifications/fleet-producer.d.ts +19 -0
  81. package/dist/notifications/fleet-producer.js +208 -0
  82. package/dist/notifications/outbox.d.ts +46 -0
  83. package/dist/notifications/outbox.js +158 -0
  84. package/dist/notifications/target-cleanup.d.ts +8 -0
  85. package/dist/notifications/target-cleanup.js +41 -0
  86. package/dist/ops.js +5 -1
  87. package/dist/owner-channel/channel.d.ts +3 -0
  88. package/dist/owner-channel/channel.js +34 -6
  89. package/dist/owner-channel/commands.d.ts +4 -0
  90. package/dist/owner-channel/commands.js +8 -0
  91. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  92. package/dist/owner-channel/ours-client.js +2 -17
  93. package/dist/paths.d.ts +2 -0
  94. package/dist/paths.js +2 -0
  95. package/dist/preset-migration.js +25 -0
  96. package/dist/resolved-plan.js +1 -0
  97. package/dist/rooms-tasks/add-member.d.ts +48 -0
  98. package/dist/rooms-tasks/add-member.js +83 -0
  99. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  100. package/dist/rooms-tasks/archived-absence.js +48 -0
  101. package/dist/rooms-tasks/cli.js +34 -5
  102. package/dist/rooms-tasks/close.d.ts +5 -2
  103. package/dist/rooms-tasks/close.js +208 -28
  104. package/dist/rooms-tasks/config.d.ts +1 -1
  105. package/dist/rooms-tasks/config.js +11 -4
  106. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  107. package/dist/rooms-tasks/cowork-adapter.js +8 -121
  108. package/dist/rooms-tasks/deletion.d.ts +1 -1
  109. package/dist/rooms-tasks/deletion.js +74 -5
  110. package/dist/rooms-tasks/erasure.d.ts +6 -0
  111. package/dist/rooms-tasks/erasure.js +169 -0
  112. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  113. package/dist/rooms-tasks/external-worker.js +3 -4
  114. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  115. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  116. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  117. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  118. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  119. package/dist/rooms-tasks/layout-cli.js +107 -0
  120. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  121. package/dist/rooms-tasks/layout-config.js +89 -0
  122. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  123. package/dist/rooms-tasks/layout-control.js +52 -0
  124. package/dist/rooms-tasks/layout-service.d.ts +42 -0
  125. package/dist/rooms-tasks/layout-service.js +185 -0
  126. package/dist/rooms-tasks/layout.d.ts +114 -0
  127. package/dist/rooms-tasks/layout.js +250 -0
  128. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  129. package/dist/rooms-tasks/live-readiness.js +85 -0
  130. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  131. package/dist/rooms-tasks/member-startup.js +2 -0
  132. package/dist/rooms-tasks/provision.d.ts +14 -0
  133. package/dist/rooms-tasks/provision.js +173 -46
  134. package/dist/rooms-tasks/room-state.d.ts +4 -2
  135. package/dist/rooms-tasks/room-state.js +53 -8
  136. package/dist/rooms-tasks/task-state.d.ts +42 -7
  137. package/dist/rooms-tasks/task-state.js +153 -23
  138. package/dist/rooms-tasks/types.d.ts +28 -0
  139. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  140. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  141. package/dist/rooms-tasks/workspace.d.ts +26 -0
  142. package/dist/rooms-tasks/workspace.js +195 -0
  143. package/dist/runner.d.ts +19 -3
  144. package/dist/runner.js +128 -45
  145. package/dist/session/acp.d.ts +9 -1
  146. package/dist/session/acp.js +31 -6
  147. package/dist/session/arbiter.d.ts +5 -0
  148. package/dist/session/arbiter.js +28 -2
  149. package/dist/session/codex-app-server.d.ts +5 -0
  150. package/dist/session/codex-app-server.js +36 -3
  151. package/dist/session/control.d.ts +12 -1
  152. package/dist/session/control.js +68 -3
  153. package/dist/session/conversation-normalizer.d.ts +2 -1
  154. package/dist/session/conversation-normalizer.js +1 -1
  155. package/dist/session/conversation-store.d.ts +1 -1
  156. package/dist/session/conversation-store.js +7 -11
  157. package/dist/session/conversation-types.d.ts +7 -3
  158. package/dist/session/task-notice.d.ts +14 -0
  159. package/dist/session/task-notice.js +13 -0
  160. package/dist/session/types.d.ts +6 -0
  161. package/dist/setup-tunnel-preflight.d.ts +34 -0
  162. package/dist/setup-tunnel-preflight.js +62 -0
  163. package/dist/socket-path.d.ts +2 -0
  164. package/dist/socket-path.js +22 -0
  165. package/dist/spawn.js +8 -1
  166. package/dist/startup-readiness.d.ts +13 -0
  167. package/dist/startup-readiness.js +38 -0
  168. package/dist/subscriptions/cli.d.ts +91 -0
  169. package/dist/subscriptions/cli.js +223 -0
  170. package/dist/subscriptions/launch.d.ts +37 -0
  171. package/dist/subscriptions/launch.js +93 -0
  172. package/dist/subscriptions/login.d.ts +75 -0
  173. package/dist/subscriptions/login.js +182 -0
  174. package/dist/subscriptions/observed.d.ts +7 -0
  175. package/dist/subscriptions/observed.js +44 -0
  176. package/dist/subscriptions/probe.d.ts +10 -0
  177. package/dist/subscriptions/probe.js +73 -0
  178. package/dist/subscriptions/service.d.ts +102 -0
  179. package/dist/subscriptions/service.js +364 -0
  180. package/dist/subscriptions/store.d.ts +71 -0
  181. package/dist/subscriptions/store.js +140 -0
  182. package/dist/supervisor/launchd.js +2 -1
  183. package/dist/supervisor/systemd.js +9 -23
  184. package/dist/temp-idle.d.ts +17 -0
  185. package/dist/temp-idle.js +42 -0
  186. package/dist/temp-lifecycle.d.ts +6 -1
  187. package/dist/temp-lifecycle.js +29 -13
  188. package/dist/tunnel-setup-completion.d.ts +7 -0
  189. package/dist/tunnel-setup-completion.js +13 -0
  190. package/dist/tunnel-setup-input.d.ts +19 -0
  191. package/dist/tunnel-setup-input.js +124 -0
  192. package/dist/web/agent-attachments.d.ts +29 -0
  193. package/dist/web/agent-attachments.js +162 -0
  194. package/dist/web/auth.d.ts +29 -1
  195. package/dist/web/auth.js +82 -7
  196. package/dist/web/available-port.d.ts +2 -0
  197. package/dist/web/available-port.js +23 -0
  198. package/dist/web/control.d.ts +3 -3
  199. package/dist/web/control.js +10 -8
  200. package/dist/web/fleet-config-service.d.ts +4 -1
  201. package/dist/web/fleet-config-service.js +46 -15
  202. package/dist/web/prefix-gateway.d.ts +32 -0
  203. package/dist/web/prefix-gateway.js +234 -0
  204. package/dist/web/runtime.d.ts +3 -1
  205. package/dist/web/runtime.js +116 -31
  206. package/dist/web/server.d.ts +16 -0
  207. package/dist/web/server.js +588 -53
  208. package/dist/web/service.d.ts +11 -2
  209. package/dist/web/service.js +30 -4
  210. package/dist/web/workspace-devices.d.ts +36 -0
  211. package/dist/web/workspace-devices.js +101 -0
  212. package/dist/web/workspace-tunnel.d.ts +1 -0
  213. package/dist/web/workspace-tunnel.js +34 -0
  214. package/dist/workspace-enrollment.d.ts +82 -0
  215. package/dist/workspace-enrollment.js +566 -0
  216. package/dist/workspace-replacement.d.ts +66 -0
  217. package/dist/workspace-replacement.js +220 -0
  218. package/docs/live-voice.md +13 -0
  219. package/docs/room-layouts.md +227 -0
  220. package/docs/task-workspaces.md +130 -0
  221. package/docs/validation/managed-cli-permissions.md +421 -0
  222. package/examples/room-layouts/fresh.yaml +27 -0
  223. package/examples/room-layouts/scoped.yaml +21 -0
  224. package/examples/room-layouts/shared.yaml +19 -0
  225. package/package.json +15 -18
  226. package/presets/README.md +78 -0
  227. package/presets/brain-catalog.json +8 -2
  228. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  229. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  230. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  231. package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
  232. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  233. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  234. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  235. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  236. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  237. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  238. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  239. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  240. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  241. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  242. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  243. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  244. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  245. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  246. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  247. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  248. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  249. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  250. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  251. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  252. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  253. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  254. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  255. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  256. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  257. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  258. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  259. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  260. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  261. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  262. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  263. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  264. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  265. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  266. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  267. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  268. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  269. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  270. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  271. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  272. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  273. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  274. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  275. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  276. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  277. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  278. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  279. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  280. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  281. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  282. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  283. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  284. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  285. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  286. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  287. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  288. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  289. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  290. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  291. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  292. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  293. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  294. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  295. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  296. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  297. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  298. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  299. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  300. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  301. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  302. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  303. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  304. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  305. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  306. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  307. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  308. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  309. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  310. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  311. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  312. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  313. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  314. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  315. package/presets/fleet/roles/Coordinator.yaml +34 -11
  316. package/presets/fleet/roles/Engineer.yaml +40 -0
  317. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  318. package/presets/fleet/room_layouts/pair.yaml +24 -0
  319. package/presets/fleet/room_layouts/single.yaml +17 -0
  320. package/presets/fleet/room_layouts/team.yaml +30 -0
  321. package/presets/fleet/room_templates/engineering.yaml +19 -0
  322. package/presets/manifest.json +2 -2
  323. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  324. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  325. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  326. package/dist/web-app/icons/ours-fleet.svg +0 -4
  327. package/dist/web-app/index.html +0 -17
  328. package/dist/web-app/manifest.webmanifest +0 -15
  329. package/dist/web-app/offline.html +0 -18
  330. package/dist/web-app/sw.js +0 -51
@@ -7,6 +7,8 @@ export function buildRoomMemberTask(input) {
7
7
  `Fleet Task ${input.taskId ?? '(standalone)'} — ${member.cowork_role} in room ${input.roomId}`,
8
8
  '',
9
9
  ];
10
+ if (input.workspace)
11
+ lines.push(`Task-owned workspace: ${input.workspace}`, 'Start and keep all execution artifacts, dependency checkouts, and Git worktrees inside this workspace.', 'Clone repositories inside this workspace before adding worktrees; never attach to an external Git common directory.', 'Use distinct per-agent branches/directories for concurrent edits. Configured cwd is source context, not artifact ownership.', 'Completion, cancellation and room retirement retain artifacts. Only explicit owner deletion removes them.', 'Do not delete the workspace or its ownership marker yourself.', '');
10
12
  if (input.goal)
11
13
  lines.push(`Goal: ${input.goal}`);
12
14
  if (input.brief)
@@ -3,6 +3,7 @@ import type { RoomOrchestrationRecord, TemplateSnapshot } from './types.js';
3
3
  import { type FleetConfig } from '../config.js';
4
4
  export declare function getBinPath(): string;
5
5
  export interface ProvisionMembersInput {
6
+ configPath?: string;
6
7
  cfg: FleetConfig;
7
8
  cowork: CoworkAdapter;
8
9
  roomId: string;
@@ -12,6 +13,8 @@ export interface ProvisionMembersInput {
12
13
  brief?: string;
13
14
  goal?: string;
14
15
  startupWait?: Partial<StartupWaitPolicy>;
16
+ /** Append only the supplied new slots to an already active room. */
17
+ append?: boolean;
15
18
  }
16
19
  export interface StartupWaitPolicy {
17
20
  timeoutMs: number;
@@ -21,6 +24,17 @@ export interface StartupWaitPolicy {
21
24
  sleep(ms: number): Promise<void>;
22
25
  }
23
26
  export declare function provisionMembers(input: ProvisionMembersInput): Promise<RoomOrchestrationRecord>;
27
+ /** Reconcile proven, admitted launches without any spawn, archive, invite or recovery path. */
28
+ export declare function reconcileExistingTaskMembers(input: {
29
+ taskId: string;
30
+ checkOnly?: boolean;
31
+ cowork: Pick<CoworkAdapter, 'getRoom'>;
32
+ expectedLaunches: ReadonlyArray<{
33
+ role_name: string;
34
+ launch_id: string;
35
+ identity_cid: string;
36
+ }>;
37
+ }): Promise<RoomOrchestrationRecord>;
24
38
  export declare function cleanupMembers(input: {
25
39
  roomId: string;
26
40
  taskId?: string;
@@ -1,4 +1,5 @@
1
1
  import { readRoomReadiness } from '../agent-ours/service.js';
2
+ import { ensureWorkspace, validateWorkspace } from './workspace.js';
2
3
  import { createHash, randomUUID } from 'node:crypto';
3
4
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
4
5
  import { join } from 'node:path';
@@ -14,7 +15,7 @@ import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js
14
15
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
15
16
  import { controlRequest } from '../session/control.js';
16
17
  import { SessionControlError } from '../session/types.js';
17
- import { closeManagedRoom } from './close.js';
18
+ import { closeManagedRoom, roomCloseLockPath, CLOSE_LOCK_STALE_MS } from './close.js';
18
19
  import { withFileLock } from '../atomic-file.js';
19
20
  import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
20
21
  import { TaskStateError, taskDeletionState } from './task-state.js';
@@ -116,6 +117,7 @@ function settingsFor(member, cfg, sealed) {
116
117
  }
117
118
  function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
118
119
  return buildRoomMemberTask({
120
+ workspace: getRoomRecord(input.roomId)?.workspace?.path,
119
121
  taskId: input.taskId,
120
122
  roomId: input.roomId,
121
123
  roomIdentityCid,
@@ -129,7 +131,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
129
131
  cowork_role: member.coworkRole,
130
132
  persona: settings.persona,
131
133
  },
132
- 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 => ({
133
135
  role_name: candidate.name,
134
136
  cowork_role: candidate.coworkRole,
135
137
  })),
@@ -142,6 +144,10 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
142
144
  try {
143
145
  const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
144
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;
145
151
  return role.identity === member.name
146
152
  && startup?.room_id === roomId
147
153
  && startup.room_identity_cid === roomIdentityCid
@@ -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,6 +336,8 @@ 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;
348
341
  const readiness = readRoomReadiness(current.room_identity_cid, member.name);
349
342
  if (!readiness || readiness.room !== roomId || readiness.invite !== seat.invite_id) {
350
343
  complete = false;
@@ -371,9 +364,41 @@ function assertCoworkRoomPolicy(room, expectedAnonymous) {
371
364
  if ((room.anonymous ?? false) !== expectedAnonymous)
372
365
  throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
373
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
+ }
374
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) || 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 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) {
375
400
  const { cfg, cowork, roomId, taskId, template } = input;
376
- // 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.
377
402
  if (taskId && taskDeletionState(taskId) !== 'none')
378
403
  throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
379
404
  const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
@@ -385,6 +410,12 @@ export async function provisionMembers(input) {
385
410
  const existing = getRoomRecord(roomId);
386
411
  if (!existing?.room_identity_cid)
387
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
+ }
388
419
  const roomIdentityCid = existing.room_identity_cid;
389
420
  const ownerSeatCid = existing.owner_seat_cid ?? null;
390
421
  const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
@@ -392,22 +423,23 @@ export async function provisionMembers(input) {
392
423
  const resuming = members.length > 0
393
424
  && members.every(member => persistedNames.has(member.name));
394
425
  if (!resuming) {
395
- advanceSaga(roomId, 'create_members', 3);
396
- updateMemberSeats(roomId, members.map(member => {
397
- const memberSettings = settings.get(member.name);
398
- const evidence = launchDefinition(memberSettings.definition);
399
- return ({
400
- role_name: member.name,
401
- slot: member.slot,
402
- cowork_role: member.coworkRole,
403
- seat_state: 'pending',
404
- launch: { state: 'pending', attempt: 0,
405
- agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
406
- agent_template: memberSettings.template,
407
- agent_template_hash: memberSettings.templateHash,
408
- updated_at: new Date().toISOString() },
409
- });
410
- }));
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);
411
443
  }
412
444
  else {
413
445
  for (const member of members) {
@@ -425,7 +457,8 @@ export async function provisionMembers(input) {
425
457
  now: input.startupWait?.now ?? Date.now,
426
458
  sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
427
459
  };
428
- advanceSaga(roomId, 'join_role_groups', 4);
460
+ if (!input.append)
461
+ advanceSaga(roomId, 'join_role_groups', 4);
429
462
  try {
430
463
  const initialRoom = await cowork.recoverRoom(roomId);
431
464
  assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
@@ -446,17 +479,31 @@ export async function provisionMembers(input) {
446
479
  provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
447
480
  }))
448
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
+ }
449
494
  const issued = await cowork.issueInvite(roomId, {
450
495
  mode: 'one_time', role: member.coworkRole, min_accepts: 1,
451
496
  });
452
- const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
453
- updateMemberSeats(roomId, seats);
454
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);
455
501
  await launchMember({
456
502
  provision: input,
457
503
  member,
458
504
  settings: settings.get(member.name),
459
505
  startup: {
506
+ workspace: existing.workspace,
460
507
  room_id: roomId,
461
508
  room_identity_cid: roomIdentityCid,
462
509
  identity_name: member.name,
@@ -470,7 +517,12 @@ export async function provisionMembers(input) {
470
517
  });
471
518
  }
472
519
  catch (error) {
473
- 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
+ }
474
526
  throw error;
475
527
  }
476
528
  }
@@ -491,20 +543,23 @@ export async function provisionMembers(input) {
491
543
  }
492
544
  throw error;
493
545
  }
494
- advanceSaga(roomId, 'wait_seats', 5);
546
+ if (!input.append)
547
+ advanceSaga(roomId, 'wait_seats', 5);
495
548
  const deadline = policy.now() + policy.timeoutMs;
496
549
  let delay = policy.initialDelayMs;
497
550
  for (;;) {
498
551
  const remote = await cowork.recoverRoom(roomId);
499
552
  assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
500
553
  const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
501
- if (reconciled.complete)
554
+ if (reconciled.complete && remote.state === 'active')
502
555
  break;
503
556
  if (policy.now() >= deadline) {
504
- advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
557
+ if (!input.append)
558
+ advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
505
559
  return getRoomRecord(roomId);
506
560
  }
507
- advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
561
+ if (!input.append)
562
+ advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
508
563
  await policy.sleep(delay);
509
564
  delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
510
565
  }
@@ -529,15 +584,87 @@ export async function provisionMembers(input) {
529
584
  };
530
585
  });
531
586
  updateTaskMembers(taskId, taskMembers);
532
- if (getTask(taskId).blocked)
587
+ if (!input.append && getTask(taskId).blocked)
533
588
  unblockTask(taskId);
534
589
  }
590
+ if (input.append)
591
+ return getRoomRecord(roomId);
535
592
  advanceSaga(roomId, 'activate', 6);
536
593
  const record = activateRoom(roomId);
537
594
  if (taskId)
538
595
  activateTask(taskId);
539
596
  return record;
540
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.toLowerCase() === current.owner_seat_cid.toLowerCase() && 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
+ }
541
668
  export async function cleanupMembers(input) {
542
669
  const room = getRoomRecord(input.roomId);
543
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 {};