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

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 +592 -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
@@ -0,0 +1,185 @@
1
+ import { randomUUID, createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadConfig, loadRoomsConfig, splitRootFor } from '../config.js';
6
+ import { agentDir, stateRoot, defaultConfigPath } from '../paths.js';
7
+ import { controlRequest } from '../session/control.js';
8
+ import { spawnTemp } from '../spawn.js';
9
+ import { tempSupervisorLiveness } from '../temp-lifecycle.js';
10
+ import { layoutSupervisorId } from './layout-control.js';
11
+ import { RoomLayout } from './layout.js';
12
+ import { assertLayoutFile, validLayoutKey, readRoomLayout, readRoomLayouts } from './layout-config.js';
13
+ import { createCoworkAdapter } from './cowork-adapter.js';
14
+ import { callRemoteLayoutBinding } from './layout-binding-client.js';
15
+ import { readClientProfile } from '../client-profile.js';
16
+ export class NativeLayoutSupervisor {
17
+ configPath;
18
+ runId;
19
+ templates;
20
+ id;
21
+ constructor(configPath, runId, templates) {
22
+ this.configPath = configPath;
23
+ this.runId = runId;
24
+ this.templates = templates;
25
+ mkdirSync(stateRoot(), { recursive: true, mode: 0o700 });
26
+ this.id = layoutSupervisorId();
27
+ }
28
+ async request(agent, temporary, request) {
29
+ if (!/^[A-Za-z0-9_-]+$/.test(agent))
30
+ throw Error('invalid agent name');
31
+ const response = await controlRequest(agentDir(agent, temporary), { command: 'layout_control', layout: request }, 60_000);
32
+ if (!response.ok)
33
+ throw Error(response.error ?? 'layout control rejected');
34
+ return response.result;
35
+ }
36
+ async inspect(agent, temporary) {
37
+ return await this.request(agent, temporary, { action: 'inspect' });
38
+ }
39
+ call(i, request) {
40
+ if (i.remote)
41
+ return callRemoteLayoutBinding(i, request, readClientProfile().expectedInstanceId);
42
+ if (i.supervisor !== this.id || !i.agent || typeof i.temporary !== 'boolean')
43
+ throw Error('invalid local layout binding');
44
+ return this.request(i.agent, i.temporary, { ...request, instance: i });
45
+ }
46
+ async verify(i) { await this.call(i, { action: 'verify' }); }
47
+ async spawn(key, template) {
48
+ const definition = this.templates[template];
49
+ if (!definition)
50
+ throw Error(`agent template not found in run snapshot: ${template}`);
51
+ const name = `layout-${createHash('sha256').update(this.runId + ':' + key).digest('hex').slice(0, 16)}`;
52
+ await spawnTemp({ name, temp: true, configPath: this.configPath, agentDefinition: definition,
53
+ creationActionId: `${this.runId}:${key}`, surface: 'cli' }, fileURLToPath(new URL('../cli.js', import.meta.url)));
54
+ const deadline = Date.now() + 120_000;
55
+ let last;
56
+ while (Date.now() < deadline) {
57
+ try {
58
+ return await this.inspect(name, true);
59
+ }
60
+ catch (e) {
61
+ last = e;
62
+ }
63
+ await new Promise(resolve => setTimeout(resolve, 250));
64
+ }
65
+ throw Error(`layout agent startup not confirmed: ${name}: ${String(last)}`);
66
+ }
67
+ async join(i, invite, roomCid, membership) {
68
+ if (i.remote) {
69
+ if (!membership)
70
+ throw Error('cross-Fleet join requires room ID and role');
71
+ await this.call(i, { action: 'join', roomCid, roomId: membership.roomId, roomRole: membership.role });
72
+ return;
73
+ }
74
+ await this.call(i, { action: 'join', invite, roomCid });
75
+ const cowork = createCoworkAdapter();
76
+ const deadline = Date.now() + 30_000;
77
+ while (Date.now() < deadline) {
78
+ const rooms = await cowork.listRooms();
79
+ if (rooms.some(r => r.identity_cid === roomCid && r.seats.some(s => s.identity_cid === i.cid && s.seat_state !== 'removed')))
80
+ return;
81
+ await new Promise(resolve => setTimeout(resolve, 100));
82
+ }
83
+ throw Error('room admission not observed');
84
+ }
85
+ async assign(i, assignment) {
86
+ await this.call(i, { action: 'assign', assignment });
87
+ }
88
+ async retire(i) {
89
+ if (i.remote)
90
+ throw Error('cannot retire a borrowed cross-Fleet instance');
91
+ if (i.supervisor !== this.id || !i.agent || !/^[A-Za-z0-9_-]+$/.test(i.agent) || i.temporary !== true)
92
+ throw Error('invalid local temporary layout instance');
93
+ const dir = agentDir(i.agent, true);
94
+ const stopped = async () => !existsSync(dir) || await tempSupervisorLiveness(dir) === 'stopped';
95
+ if (await stopped())
96
+ return;
97
+ try {
98
+ await this.call(i, { action: 'retire' });
99
+ }
100
+ catch (error) {
101
+ if (!await stopped())
102
+ throw error;
103
+ }
104
+ }
105
+ }
106
+ export const layoutRunPath = (id) => {
107
+ if (!validLayoutKey(id))
108
+ throw Error('invalid layout run ID');
109
+ const root = join(stateRoot(), 'layouts');
110
+ mkdirSync(root, { recursive: true, mode: 0o700 });
111
+ assertLayoutFile(root, true);
112
+ return join(root, id + '.json');
113
+ };
114
+ export class RoomLayoutService {
115
+ configPath;
116
+ constructor(configPath) {
117
+ this.configPath = configPath;
118
+ }
119
+ list() {
120
+ const layouts = readRoomLayouts(join(splitRootFor(this.configPath ?? defaultConfigPath()), 'room_layouts'), []);
121
+ const cfg = loadConfig(this.configPath);
122
+ for (const [name, layout] of Object.entries(layouts))
123
+ for (const participant of Object.values(layout.participants))
124
+ if (participant.agent_template && !cfg.agentTemplates?.[participant.agent_template])
125
+ throw Error(`room layout ${name}: agent template not found: ${participant.agent_template}`);
126
+ return layouts;
127
+ }
128
+ /** Reads only the named file, so an unrelated broken layout cannot block this one. */
129
+ definition(name) {
130
+ if (!validLayoutKey(name))
131
+ throw Error(`room layout not found: ${name}`);
132
+ const root = join(splitRootFor(this.configPath ?? defaultConfigPath()), 'room_layouts');
133
+ const files = existsSync(root) ? (assertLayoutFile(root, true), readdirSync(root).filter(f => f === `${name}.yaml` || f === `${name}.yml`)) : [];
134
+ if (files.length > 1)
135
+ throw Error(`duplicate room layout: ${name}`);
136
+ if (!files.length)
137
+ throw Error(`room layout not found: ${name}`);
138
+ const def = readRoomLayout(join(root, files[0]));
139
+ const cfg = loadConfig(this.configPath);
140
+ for (const participant of Object.values(def.participants))
141
+ if (participant.agent_template && !cfg.agentTemplates?.[participant.agent_template])
142
+ throw Error(`room layout ${name}: agent template not found: ${participant.agent_template}`);
143
+ return def;
144
+ }
145
+ supervisor(id = 'inspect', templates = {}) {
146
+ return new NativeLayoutSupervisor(this.configPath, id, templates);
147
+ }
148
+ owner() {
149
+ const rooms = loadRoomsConfig(this.configPath);
150
+ if (rooms.defaults?.attach_owner === false)
151
+ return undefined;
152
+ if (!rooms._invite)
153
+ throw Error('room owner invite required; configure rooms.owner or explicitly set rooms.defaults.attach_owner: false');
154
+ return { cid: rooms.owner.expected_cid, role: rooms.owner.role ?? 'Owner', invite: rooms._invite.value };
155
+ }
156
+ engine(id, cleanup = false) {
157
+ const path = layoutRunPath(id);
158
+ if (existsSync(path))
159
+ assertLayoutFile(path);
160
+ return new RoomLayout(path, () => createCoworkAdapter(), this.supervisor(id), cleanup ? undefined : this.owner());
161
+ }
162
+ open(id, cleanup = false) {
163
+ const initial = this.engine(id, cleanup), snapshot = initial.snapshot();
164
+ return new RoomLayout(layoutRunPath(id), () => createCoworkAdapter(), this.supervisor(id, snapshot.agent_templates ?? {}), cleanup ? undefined : this.owner());
165
+ }
166
+ /** `scope` lets an owner (e.g. a Task) snapshot an adjusted definition and pin agent working directories. */
167
+ async create(name, bindings = {}, id = `run-${randomUUID()}`, scope = {}) {
168
+ const cfg = loadConfig(this.configPath);
169
+ // A caller that already verified the source passes it, so no second read can race an edit.
170
+ const source = scope.source ?? this.definition(name);
171
+ const definition = scope.definition ? scope.definition(structuredClone(source)) : source;
172
+ const templates = {};
173
+ for (const [key, participant] of Object.entries(definition.participants)) {
174
+ if (bindings[key] || !participant.agent_template)
175
+ continue;
176
+ const template = cfg.agentTemplates?.[participant.agent_template];
177
+ if (!template)
178
+ throw Error(`agent template not found: ${participant.agent_template}`);
179
+ templates[participant.agent_template] = { ...structuredClone(template), ...(scope.cwd ? { cwd: scope.cwd } : {}) };
180
+ }
181
+ const engine = this.engine(id);
182
+ await engine.create(definition, layoutSupervisorId(), bindings, templates);
183
+ return { id, state: engine.snapshot() };
184
+ }
185
+ }
@@ -0,0 +1,114 @@
1
+ import type { CoworkAdapter, CoworkRoomCreateResult } from './cowork-adapter.js';
2
+ export interface LayoutOwner {
3
+ cid: string;
4
+ role: string;
5
+ invite: string;
6
+ }
7
+ declare const ownerReference: (owner: LayoutOwner) => {
8
+ cid: string;
9
+ role: string;
10
+ fingerprint: string;
11
+ };
12
+ export interface LayoutInstance {
13
+ supervisor: string;
14
+ launch: string;
15
+ cid: string;
16
+ session: string;
17
+ agent?: string;
18
+ temporary?: boolean;
19
+ remote?: {
20
+ url: string;
21
+ grant_id: string;
22
+ credential_file: string;
23
+ daemon_instance_id: string;
24
+ };
25
+ }
26
+ export interface LayoutParticipant {
27
+ agent_template?: string;
28
+ instance_scope?: 'layout' | 'room';
29
+ }
30
+ export interface LayoutRoom {
31
+ goal: string;
32
+ members: string[];
33
+ contract?: string;
34
+ quiet_membership?: boolean;
35
+ anonymous?: boolean;
36
+ roles?: Record<string, string>;
37
+ }
38
+ export interface LayoutDefinition {
39
+ participants: Record<string, LayoutParticipant>;
40
+ rooms: Record<string, LayoutRoom>;
41
+ }
42
+ export interface LayoutAssignment {
43
+ id: string;
44
+ room_id: string;
45
+ room_cid: string;
46
+ goal: string;
47
+ participant: string;
48
+ room_role?: string;
49
+ contract?: string;
50
+ }
51
+ export interface LayoutSupervisor {
52
+ id: string;
53
+ /** Must verify current launch, CID, live session and standalone ownership. */
54
+ verify(instance: LayoutInstance): Promise<void>;
55
+ spawn(key: string, template: string): Promise<LayoutInstance>;
56
+ join(instance: LayoutInstance, invite: string, roomCid: string, membership?: {
57
+ roomId: string;
58
+ role: string;
59
+ }): Promise<void>;
60
+ assign(instance: LayoutInstance, assignment: LayoutAssignment): Promise<void>;
61
+ /** Retire the exact instance, or confirm it is already stopped; never stop a replacement. */
62
+ retire(instance: LayoutInstance): Promise<void>;
63
+ }
64
+ type ParticipantState = {
65
+ instance?: LayoutInstance;
66
+ owned: boolean;
67
+ retired?: boolean;
68
+ participant?: string;
69
+ room?: string;
70
+ };
71
+ type RoomState = {
72
+ spec: LayoutRoom;
73
+ native?: CoworkRoomCreateResult;
74
+ ready: string[];
75
+ state: 'provisioning' | 'active' | 'closed';
76
+ owner_ready?: boolean;
77
+ };
78
+ export interface RoomLayoutState {
79
+ version: 1;
80
+ owner?: ReturnType<typeof ownerReference>;
81
+ agent_templates?: Record<string, import('../config.js').AgentTemplateDefinition>;
82
+ definition: LayoutDefinition;
83
+ controller: string;
84
+ participants: Record<string, ParticipantState>;
85
+ rooms: Record<string, RoomState>;
86
+ order: string[];
87
+ closed: boolean;
88
+ closing?: boolean;
89
+ uncertain?: string;
90
+ }
91
+ export declare function validateLayout(def: LayoutDefinition): void;
92
+ export declare class RoomLayout {
93
+ private file;
94
+ private supervisor;
95
+ private owner?;
96
+ private coworkAdapter?;
97
+ private readonly coworkFactory;
98
+ constructor(file: string, cowork: CoworkAdapter | (() => CoworkAdapter), supervisor: LayoutSupervisor, owner?: LayoutOwner | undefined);
99
+ private get cowork();
100
+ snapshot(): RoomLayoutState;
101
+ private save;
102
+ private verify;
103
+ create(definition: LayoutDefinition, controller: string, bindings?: Record<string, LayoutInstance>, agentTemplates?: RoomLayoutState['agent_templates']): Promise<void>;
104
+ private checkOwnerRoles;
105
+ private operation;
106
+ private mutation;
107
+ private activateRoom;
108
+ activate(actor: string, key: string): Promise<void>;
109
+ private assertRoomActive;
110
+ private archiveRoom;
111
+ closeRoom(actor: string, key: string): Promise<void>;
112
+ close(actor: string): Promise<void>;
113
+ }
114
+ export {};
@@ -0,0 +1,250 @@
1
+ /** Durable local room-layout controller.
2
+ * Uses native Cowork operations and a supervising Fleet's exact-instance port.
3
+ * Unknown mutation outcomes stop for reconciliation; they are never replayed.
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { canonicalJson } from '../canonical-json.js';
8
+ import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
9
+ const ownerReference = (owner) => ({ cid: owner.cid, role: owner.role, fingerprint: createHash('sha256').update(owner.invite).digest('hex') });
10
+ const own = (o, key) => Object.hasOwn(o, key);
11
+ function need(ok, message) { if (!ok)
12
+ throw Error(message); }
13
+ /** Room/participant keys cannot contain ':', so scoped instance keys cannot collide. */
14
+ function instanceKey(definition, participant, room) {
15
+ return definition.participants[participant].instance_scope === 'room' ? `${room}:${participant}` : participant;
16
+ }
17
+ export function validateLayout(def) {
18
+ need(Object.keys(def.rooms).length && Object.keys(def.participants).length, 'rooms and participants required');
19
+ for (const [key, participant] of Object.entries(def.participants)) {
20
+ need(participant.instance_scope === undefined || ['layout', 'room'].includes(participant.instance_scope), `invalid instance_scope for ${key}`);
21
+ if (participant.instance_scope === 'room')
22
+ need(typeof participant.agent_template === 'string' && participant.agent_template.trim(), `room-scoped participant ${key} requires agent_template`);
23
+ }
24
+ for (const [key, room] of Object.entries(def.rooms)) {
25
+ need(key.length && room.goal.trim() && room.members.length, 'room goal and members required');
26
+ need(new Set(room.members).size === room.members.length, 'duplicate participant in room');
27
+ for (const member of room.members)
28
+ need(own(def.participants, member), `unknown participant ${member}`);
29
+ }
30
+ for (const key of [...Object.keys(def.rooms), ...Object.keys(def.participants)])
31
+ need(/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(key) && !['constructor', 'prototype', '__proto__'].includes(key), 'invalid layout key');
32
+ }
33
+ export class RoomLayout {
34
+ file;
35
+ supervisor;
36
+ owner;
37
+ coworkAdapter;
38
+ coworkFactory;
39
+ constructor(file, cowork, supervisor, owner) {
40
+ this.file = file;
41
+ this.supervisor = supervisor;
42
+ this.owner = owner;
43
+ this.coworkFactory = typeof cowork === 'function' ? cowork : () => cowork;
44
+ }
45
+ get cowork() { return this.coworkAdapter ??= this.coworkFactory(); }
46
+ snapshot() { return JSON.parse(readFileSync(this.file, 'utf8')); }
47
+ save(s) { replaceFileAtomically(this.file, JSON.stringify(s, null, 2)); }
48
+ async verify(i) {
49
+ need(i.supervisor === this.supervisor.id || i.remote, 'remote binding unsupported');
50
+ need(i.launch && i.cid && i.session, 'incomplete instance reference');
51
+ await this.supervisor.verify(i);
52
+ }
53
+ async create(definition, controller, bindings = {}, agentTemplates) {
54
+ validateLayout(definition);
55
+ need(controller, 'controller required');
56
+ this.checkOwnerRoles(definition);
57
+ await withFileLock(this.file + '.lock', async () => {
58
+ need(!existsSync(this.file), 'layout instance already exists');
59
+ for (const key of Object.keys(bindings)) {
60
+ need(own(definition.participants, key), 'unknown binding');
61
+ need(definition.participants[key].instance_scope !== 'room', `binding unsupported for room-scoped participant ${key}`);
62
+ }
63
+ // Validate every supplied binding before any external mutation.
64
+ for (const [key, p] of Object.entries(definition.participants)) {
65
+ need(bindings[key] || p.agent_template, `binding required for ${key}`);
66
+ if (bindings[key])
67
+ await this.verify(bindings[key]);
68
+ }
69
+ for (const room of Object.values(definition.rooms)) {
70
+ const cids = room.members.flatMap(key => bindings[key] ? [bindings[key].cid] : []);
71
+ need(new Set(cids).size === cids.length, 'same instance cannot occupy two seats in one room');
72
+ }
73
+ const participants = {};
74
+ for (const [key, participant] of Object.entries(definition.participants)) {
75
+ if (participant.instance_scope === 'room') {
76
+ for (const [room, spec] of Object.entries(definition.rooms))
77
+ if (spec.members.includes(key))
78
+ participants[instanceKey(definition, key, room)] = { owned: true, participant: key, room };
79
+ }
80
+ else
81
+ participants[key] = { owned: !bindings[key], ...(bindings[key] ? { instance: bindings[key] } : {}) };
82
+ }
83
+ this.save({ version: 1, definition, controller, ...(this.owner ? { owner: ownerReference(this.owner) } : {}),
84
+ agent_templates: agentTemplates, participants, rooms: {}, order: [], closed: false });
85
+ });
86
+ }
87
+ checkOwnerRoles(definition) {
88
+ if (!this.owner)
89
+ return;
90
+ for (const room of Object.values(definition.rooms))
91
+ need(room.members.every(member => (room.roles?.[member] ?? member) !== this.owner.role), 'participant cannot use the owner role');
92
+ }
93
+ async operation(actor, work, cleanup = false) {
94
+ return withFileLock(this.file + '.lock', async () => {
95
+ const s = this.snapshot();
96
+ need(actor === s.controller, 'controller authorization required');
97
+ if (!cleanup) {
98
+ need(!s.closed && !s.closing, 'layout instance closing or closed');
99
+ need(!s.uncertain, `reconciliation required: ${s.uncertain}`);
100
+ }
101
+ return work(s);
102
+ });
103
+ }
104
+ async mutation(s, id, work, commit) {
105
+ s.uncertain = id;
106
+ this.save(s);
107
+ // On any error keep the cursor, even if the operation may have succeeded.
108
+ const result = await work();
109
+ commit(result);
110
+ delete s.uncertain;
111
+ this.save(s);
112
+ }
113
+ async activateRoom(s, key, spec) {
114
+ if (s.owner)
115
+ need(this.owner && canonicalJson(s.owner) === canonicalJson(ownerReference(this.owner)), 'room owner configuration changed');
116
+ this.checkOwnerRoles(s.definition);
117
+ need(s.rooms[key]?.state !== 'closed', 'room closed');
118
+ for (const member of spec.members) {
119
+ const p = s.participants[instanceKey(s.definition, member, key)];
120
+ const i = p?.instance;
121
+ if (i)
122
+ await this.verify(i);
123
+ need(!p?.retired, 'participant retired');
124
+ }
125
+ if (s.rooms[key]?.state === 'active') {
126
+ await this.assertRoomActive(s, key);
127
+ return;
128
+ }
129
+ const room = s.rooms[key] ??= { spec, state: 'provisioning', ready: [] };
130
+ this.save(s);
131
+ for (const member of spec.members) {
132
+ const resolvedKey = instanceKey(s.definition, member, key), p = s.participants[resolvedKey];
133
+ if (!p.instance) {
134
+ const template = s.definition.participants[member]?.agent_template;
135
+ need(template, `no factory for ${member}`);
136
+ await this.mutation(s, `spawn:${resolvedKey}`, () => this.supervisor.spawn(resolvedKey, template), i => { p.instance = i; });
137
+ await this.verify(p.instance);
138
+ }
139
+ }
140
+ const roleFor = (member) => spec.roles?.[member] ?? member;
141
+ if (!room.native)
142
+ await this.mutation(s, `create:${key}`, () => this.cowork.createRoom({
143
+ room_name: `${key.slice(0, 24)}-${createHash('sha256').update(this.file + '\0' + key).digest('hex').slice(0, 24)}`,
144
+ goal: spec.goal, briefing: JSON.stringify({ goal: spec.goal, contract: spec.contract }),
145
+ quiet_membership: spec.quiet_membership ?? true, anonymous: spec.anonymous ?? false,
146
+ activation_requirements: [...new Set(spec.members.map(roleFor))].map(role => ({ role, count: spec.members.filter(m => roleFor(m) === role).length })),
147
+ }), native => { room.native = native; s.order.push(key); });
148
+ if (s.owner && !room.owner_ready)
149
+ await this.mutation(s, `owner:${key}`, async () => {
150
+ await this.cowork.acceptInvite(room.native.room_id, this.owner.invite, { role: s.owner.role, expected_cid: s.owner.cid });
151
+ await this.cowork.setRoleCommands(room.native.room_id, { role: s.owner.role, commands: ['*'] });
152
+ }, () => { room.owner_ready = true; });
153
+ for (const member of spec.members) {
154
+ if (room.ready.includes(member))
155
+ continue;
156
+ const i = s.participants[instanceKey(s.definition, member, key)].instance;
157
+ await this.verify(i);
158
+ const seats = await this.cowork.getSeats(room.native.room_id);
159
+ if (!seats.some(seat => seat.identity_cid === i.cid && seat.role === roleFor(member) && seat.seat_state !== 'removed')) {
160
+ await this.mutation(s, `admit:${key}:${member}`, async () => {
161
+ const invite = i.remote ? '' : (await this.cowork.issueInvite(room.native.room_id, { mode: 'one_time', role: roleFor(member), min_accepts: 1 })).invite;
162
+ await this.supervisor.join(i, invite, room.native.identity_cid, { roomId: room.native.room_id, role: roleFor(member) });
163
+ }, () => { });
164
+ }
165
+ }
166
+ await this.assertRoomActive(s, key);
167
+ // Admission of all required seats precedes any participant work.
168
+ for (const member of spec.members) {
169
+ if (room.ready.includes(member))
170
+ continue;
171
+ const i = s.participants[instanceKey(s.definition, member, key)].instance;
172
+ await this.verify(i);
173
+ await this.mutation(s, `assignment:${key}:${member}`, () => this.supervisor.assign(i, {
174
+ id: `${key}:${member}`, room_id: room.native.room_id, room_cid: room.native.identity_cid,
175
+ participant: member, room_role: roleFor(member), goal: spec.goal, contract: spec.contract,
176
+ }), () => { room.ready.push(member); });
177
+ }
178
+ room.state = 'active';
179
+ this.save(s);
180
+ }
181
+ activate(actor, key) {
182
+ return this.operation(actor, async (s) => {
183
+ need(own(s.definition.rooms, key), 'unknown room');
184
+ await this.activateRoom(s, key, s.definition.rooms[key]);
185
+ });
186
+ }
187
+ async assertRoomActive(s, key) {
188
+ const room = s.rooms[key], native = await this.cowork.getRoom(room.native.room_id);
189
+ need(native?.identity_cid === room.native.identity_cid && native.state === 'active', 'native room missing, changed or inactive');
190
+ if (s.owner)
191
+ need(native.seats.some(seat => seat.identity_cid.toLowerCase() === s.owner.cid.toLowerCase()
192
+ && seat.role === s.owner.role && seat.seat_state === 'active'), 'owner membership not active');
193
+ need(room.spec.members.every(member => native.seats.some(seat => seat.identity_cid === s.participants[instanceKey(s.definition, member, key)].instance.cid
194
+ && seat.role === (room.spec.roles?.[member] ?? member) && seat.seat_state === 'active')), 'membership not active');
195
+ }
196
+ async archiveRoom(s, key) {
197
+ const room = s.rooms[key];
198
+ need(room?.native, 'room not created');
199
+ if (room.state === 'closed')
200
+ return;
201
+ const native = await this.cowork.getRoom(room.native.room_id);
202
+ if (native) {
203
+ need(native.identity_cid === room.native.identity_cid, 'native room identity changed');
204
+ if (native.state !== 'closed')
205
+ await this.cowork.closeRoom(room.native.room_id);
206
+ }
207
+ room.state = 'closed';
208
+ this.save(s);
209
+ }
210
+ closeRoom(actor, key) {
211
+ return this.operation(actor, s => this.archiveRoom(s, key), true);
212
+ }
213
+ close(actor) {
214
+ return this.operation(actor, async (s) => {
215
+ if (s.closed)
216
+ return;
217
+ s.closing = true;
218
+ this.save(s);
219
+ const errors = [];
220
+ for (const key of [...s.order].reverse()) {
221
+ try {
222
+ await this.archiveRoom(s, key);
223
+ }
224
+ catch (error) {
225
+ errors.push(`room ${key}: ${String(error)}`);
226
+ }
227
+ }
228
+ for (const [key, p] of Object.entries(s.participants))
229
+ if (p.owned && p.instance && !p.retired) {
230
+ try {
231
+ need(p.instance.supervisor === this.supervisor.id, 'remote binding unsupported');
232
+ await this.supervisor.retire(p.instance);
233
+ p.retired = true;
234
+ this.save(s);
235
+ }
236
+ catch (error) {
237
+ errors.push(`participant ${key}: ${String(error)}`);
238
+ }
239
+ }
240
+ // Clean known resources without erasing evidence of an unknown creation outcome.
241
+ if (s.uncertain)
242
+ errors.push(`known resources cleaned where possible; inspection still required: ${s.uncertain}`);
243
+ if (errors.length)
244
+ throw Error(`layout cleanup incomplete: ${errors.join('; ')}`);
245
+ s.closed = true;
246
+ delete s.closing;
247
+ this.save(s);
248
+ }, true);
249
+ }
250
+ }
@@ -0,0 +1,18 @@
1
+ import { readRoomReadiness } from '../agent-ours/service.js';
2
+ import { controlRequest } from '../session/control.js';
3
+ import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
4
+ import type { CoworkAdapter } from './cowork-adapter.js';
5
+ import type { RoomOrchestrationRecord, TaskRecord } from './types.js';
6
+ export type TaskReadinessIssue = {
7
+ state: 'degraded' | 'unknown';
8
+ reason: string;
9
+ member?: string;
10
+ };
11
+ export interface LiveReadinessDeps {
12
+ supervisor?: typeof readTempSupervisor;
13
+ liveness?: typeof tempSupervisorLiveness;
14
+ readiness?: typeof readRoomReadiness;
15
+ control?: typeof controlRequest;
16
+ }
17
+ /** Read-only observation. A failed probe never grants authority to replace a seat. */
18
+ export declare function taskLiveReadiness(task: TaskRecord, room: RoomOrchestrationRecord | undefined, cowork: Pick<CoworkAdapter, 'getRoom'>, deps?: LiveReadinessDeps): Promise<TaskReadinessIssue | undefined>;
@@ -0,0 +1,85 @@
1
+ import { realExec } from '../exec.js';
2
+ import { agentDir } from '../paths.js';
3
+ import { readRoomReadiness } from '../agent-ours/service.js';
4
+ import { controlRequest } from '../session/control.js';
5
+ import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
6
+ /** Read-only observation. A failed probe never grants authority to replace a seat. */
7
+ export async function taskLiveReadiness(task, room, cowork, deps = {}) {
8
+ const degraded = (reason, member) => ({ state: 'degraded', reason, member });
9
+ const unknown = (reason, member) => ({ state: 'unknown', reason, member });
10
+ if (!room || task.state !== 'active' || task.room_id !== room.room_id || room.state !== 'active' || room.close || task.terminal_intent
11
+ || room.task_id !== task.task_id || !room.room_identity_cid
12
+ || task.room_identity_cid !== room.room_identity_cid)
13
+ return degraded('room_record_mismatch');
14
+ const baseExpected = room.template_snapshot?.members.reduce((n, member) => n + member.count, 0);
15
+ const expected = baseExpected === undefined ? undefined : baseExpected + (room.additional_member_templates ?? []).reduce((n, t) => n + t.members.reduce((m, s) => m + s.count, 0), 0);
16
+ if (expected === undefined || room.member_seats.length !== expected
17
+ || new Set(room.member_seats.map(seat => seat.role_name)).size !== expected
18
+ || new Set(room.member_seats.map(seat => seat.identity_cid)).size !== expected)
19
+ return degraded('member_roster_mismatch');
20
+ let remote;
21
+ try {
22
+ remote = await cowork.getRoom(room.room_id);
23
+ }
24
+ catch {
25
+ return unknown('room_probe_unavailable');
26
+ }
27
+ if (!remote || remote.room_id !== room.room_id || remote.identity_cid !== room.room_identity_cid
28
+ || remote.state !== 'active')
29
+ return degraded('room_identity_or_state_mismatch');
30
+ if (room.owner_seat_cid && remote.seats.filter(seat => seat.identity_cid.toLowerCase() === room.owner_seat_cid.toLowerCase() && seat.seat_state === 'active').length !== 1)
31
+ return degraded('owner_seat_missing');
32
+ for (const seat of room.member_seats) {
33
+ const member = seat.role_name;
34
+ if (!/^[A-Za-z0-9_-]+$/.test(member) || seat.seat_state !== 'active' || !seat.identity_cid || !seat.invite_id
35
+ || seat.launch?.state !== 'launched' || !seat.launch.launch_id || seat.retirement)
36
+ return degraded('member_record_incomplete', member);
37
+ const matches = remote.seats.filter(item => item.display_name === member && item.seat_state !== 'removed');
38
+ if (matches.length !== 1 || matches[0].identity_cid !== seat.identity_cid
39
+ || matches[0].invite_id !== seat.invite_id || matches[0].role !== seat.cowork_role
40
+ || matches[0].seat_state !== 'active')
41
+ return degraded('member_seat_mismatch', member);
42
+ const dir = agentDir(member, true);
43
+ try {
44
+ const supervisor = (deps.supervisor ?? readTempSupervisor)(dir);
45
+ if (!supervisor)
46
+ return unknown('supervisor_record_missing', member);
47
+ if (supervisor.role !== member || supervisor.launchId !== seat.launch.launch_id)
48
+ return degraded('supervisor_launch_mismatch', member);
49
+ const live = await (deps.liveness ?? tempSupervisorLiveness)(dir, {
50
+ exec: (cmd, args, opts) => realExec(cmd, args, { ...opts, timeout: 2_000 }),
51
+ });
52
+ if (live === 'stopped')
53
+ return degraded('supervisor_stopped', member);
54
+ if (live !== 'running')
55
+ return unknown('supervisor_liveness_unknown', member);
56
+ const ready = (deps.readiness ?? readRoomReadiness)(room.room_identity_cid, member);
57
+ if (!ready || ready.room !== room.room_id || ready.invite !== seat.invite_id || ready.cid !== seat.identity_cid)
58
+ return degraded('member_readiness_mismatch', member);
59
+ }
60
+ catch {
61
+ return unknown('supervisor_probe_unavailable', member);
62
+ }
63
+ try {
64
+ const response = await (deps.control ?? controlRequest)(dir, { command: 'status', controller: false }, 2_000);
65
+ if (!response.ok)
66
+ return unknown('control_unavailable', member);
67
+ const status = response.result;
68
+ if (status?.alive === false || status?.readiness === 'failed')
69
+ return degraded('agent_not_ready', member);
70
+ if (status?.alive !== true || !['idle', 'running', 'awaiting_permission'].includes(status.readiness ?? ''))
71
+ return unknown('agent_readiness_unknown', member);
72
+ const after = (deps.supervisor ?? readTempSupervisor)(dir);
73
+ if (!after || after.role !== member || after.launchId !== seat.launch.launch_id)
74
+ return degraded('supervisor_launch_changed', member);
75
+ }
76
+ catch {
77
+ return unknown('control_unavailable', member);
78
+ }
79
+ }
80
+ const memberCids = new Set(room.member_seats.map(seat => seat.identity_cid));
81
+ const memberRoles = new Set(room.member_seats.map(seat => seat.cowork_role));
82
+ if (remote.seats.some(seat => seat.seat_state === 'active' && memberRoles.has(seat.role)
83
+ && !memberCids.has(seat.identity_cid) && seat.identity_cid.toLowerCase() !== room.owner_seat_cid?.toLowerCase()))
84
+ return degraded('untracked_member_seats');
85
+ }
@@ -6,6 +6,7 @@ export interface RoomTaskMember {
6
6
  }
7
7
  /** Build the complete task text Fleet places directly in a temporary member's briefing. */
8
8
  export declare function buildRoomMemberTask(input: {
9
+ workspace?: string;
9
10
  taskId?: string;
10
11
  roomId: string;
11
12
  roomIdentityCid: string;