@ours.network/fleet 1.2.0-nightly.9 → 2.0.0

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 (429) hide show
  1. package/README.md +280 -68
  2. package/dist/account-origin.d.ts +5 -0
  3. package/dist/account-origin.js +31 -0
  4. package/dist/agent-ours/bridge.js +26 -9
  5. package/dist/agent-ours/correspondence.d.ts +10 -0
  6. package/dist/agent-ours/correspondence.js +1 -0
  7. package/dist/agent-ours/file-rpc.d.ts +12 -0
  8. package/dist/agent-ours/file-rpc.js +18 -4
  9. package/dist/agent-ours/harness.js +9 -1
  10. package/dist/agent-ours/mcp-endpoint.d.ts +22 -0
  11. package/dist/agent-ours/mcp-endpoint.js +101 -25
  12. package/dist/agent-ours/runtime.d.ts +9 -1
  13. package/dist/agent-ours/runtime.js +50 -2
  14. package/dist/agent-ours/service.d.ts +27 -1
  15. package/dist/agent-ours/service.js +107 -13
  16. package/dist/application/codex-models.d.ts +15 -0
  17. package/dist/application/codex-models.js +56 -0
  18. package/dist/application/conversation-history.d.ts +31 -0
  19. package/dist/application/conversation-history.js +339 -0
  20. package/dist/application/conversation-role-observer.d.ts +62 -0
  21. package/dist/application/conversation-role-observer.js +206 -0
  22. package/dist/application/fleet-query-service.d.ts +1 -1
  23. package/dist/application/fleet-query-service.js +3 -3
  24. package/dist/application/log-service.js +8 -8
  25. package/dist/application/model-catalog.d.ts +1 -1
  26. package/dist/application/model-catalog.js +5 -2
  27. package/dist/application/preset-provenance.d.ts +19 -0
  28. package/dist/application/preset-provenance.js +59 -0
  29. package/dist/application/resource-observer.d.ts +43 -0
  30. package/dist/application/resource-observer.js +323 -0
  31. package/dist/application/role-creation-service.js +3 -2
  32. package/dist/application/role-removal-service.js +17 -3
  33. package/dist/application/role-repository.d.ts +3 -1
  34. package/dist/application/role-repository.js +89 -54
  35. package/dist/application/room-layout-definitions.d.ts +45 -0
  36. package/dist/application/room-layout-definitions.js +130 -0
  37. package/dist/application/session-control.d.ts +63 -17
  38. package/dist/application/session-control.js +221 -23
  39. package/dist/application/supervisor-ours-tools.js +5 -11
  40. package/dist/application/task-layouts.d.ts +91 -0
  41. package/dist/application/task-layouts.js +261 -0
  42. package/dist/application/task-room-service.d.ts +101 -3
  43. package/dist/application/task-room-service.js +286 -33
  44. package/dist/application/types.d.ts +2 -0
  45. package/dist/atomic-file.d.ts +34 -1
  46. package/dist/atomic-file.js +161 -18
  47. package/dist/briefing.d.ts +3 -1
  48. package/dist/briefing.js +120 -31
  49. package/dist/build-info.json +10 -5
  50. package/dist/capabilities.d.ts +9 -1
  51. package/dist/capabilities.js +13 -0
  52. package/dist/cli.js +391 -38
  53. package/dist/client-profile.d.ts +5 -17
  54. package/dist/client-profile.js +4 -127
  55. package/dist/config.d.ts +45 -0
  56. package/dist/config.js +57 -6
  57. package/dist/creation.d.ts +1 -1
  58. package/dist/creation.js +4 -4
  59. package/dist/daemon-recovery.d.ts +14 -9
  60. package/dist/daemon-recovery.js +124 -115
  61. package/dist/docs.d.ts +1 -1
  62. package/dist/docs.js +214 -20
  63. package/dist/doctor.d.ts +4 -1
  64. package/dist/doctor.js +75 -60
  65. package/dist/erased-resources.d.ts +9 -0
  66. package/dist/erased-resources.js +87 -0
  67. package/dist/file-delivery/reader.d.ts +15 -0
  68. package/dist/file-delivery/reader.js +63 -0
  69. package/dist/file-delivery/store.d.ts +15 -0
  70. package/dist/file-delivery/store.js +124 -0
  71. package/dist/file-delivery/types.d.ts +19 -0
  72. package/dist/file-delivery/types.js +1 -0
  73. package/dist/fleet-command-audit.d.ts +13 -0
  74. package/dist/fleet-command-audit.js +31 -9
  75. package/dist/harness/agent-session.d.ts +4 -0
  76. package/dist/harness/claude-code-session.js +1 -1
  77. package/dist/harness/claude-code.js +21 -11
  78. package/dist/harness/claude-runtime.d.ts +12 -0
  79. package/dist/harness/claude-runtime.js +43 -0
  80. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  81. package/dist/harness/codex-app-server-proxy.js +41 -5
  82. package/dist/harness/codex-reasoning.d.ts +5 -0
  83. package/dist/harness/codex-reasoning.js +12 -0
  84. package/dist/harness/codex-runtime.d.ts +11 -1
  85. package/dist/harness/codex-runtime.js +31 -4
  86. package/dist/harness/codex-session.js +2 -2
  87. package/dist/harness/codex.js +24 -12
  88. package/dist/init-wizard.d.ts +2 -0
  89. package/dist/init-wizard.js +38 -19
  90. package/dist/isolation/policy.js +6 -1
  91. package/dist/isolation/types.d.ts +5 -0
  92. package/dist/managed-cli.d.ts +253 -0
  93. package/dist/managed-cli.js +923 -0
  94. package/dist/managed-recovery.d.ts +13 -0
  95. package/dist/managed-recovery.js +31 -0
  96. package/dist/minimal-setup.d.ts +2 -0
  97. package/dist/minimal-setup.js +44 -0
  98. package/dist/monitor.d.ts +9 -34
  99. package/dist/monitor.js +90 -153
  100. package/dist/notifications/fleet-producer.d.ts +31 -0
  101. package/dist/notifications/fleet-producer.js +351 -0
  102. package/dist/notifications/outbox.d.ts +59 -0
  103. package/dist/notifications/outbox.js +199 -0
  104. package/dist/notifications/target-cleanup.d.ts +16 -0
  105. package/dist/notifications/target-cleanup.js +75 -0
  106. package/dist/notifications/task-producer.d.ts +20 -0
  107. package/dist/notifications/task-producer.js +100 -0
  108. package/dist/ops.d.ts +2 -0
  109. package/dist/ops.js +20 -3
  110. package/dist/owner-channel/channel.js +17 -6
  111. package/dist/owner-channel/commands.d.ts +4 -0
  112. package/dist/owner-channel/commands.js +8 -0
  113. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  114. package/dist/owner-channel/ours-client.js +2 -17
  115. package/dist/paths.d.ts +5 -0
  116. package/dist/paths.js +5 -0
  117. package/dist/preset-migration.js +55 -0
  118. package/dist/process-incarnation.d.ts +23 -0
  119. package/dist/process-incarnation.js +42 -0
  120. package/dist/reminders/manager.d.ts +75 -0
  121. package/dist/reminders/manager.js +278 -0
  122. package/dist/reminders/runtime.d.ts +27 -0
  123. package/dist/reminders/runtime.js +32 -0
  124. package/dist/reminders/schedule.d.ts +37 -0
  125. package/dist/reminders/schedule.js +104 -0
  126. package/dist/reminders/store.d.ts +341 -0
  127. package/dist/reminders/store.js +762 -0
  128. package/dist/reminders/tools.d.ts +20 -0
  129. package/dist/reminders/tools.js +114 -0
  130. package/dist/resolved-plan.js +1 -0
  131. package/dist/rooms-tasks/add-member.d.ts +48 -0
  132. package/dist/rooms-tasks/add-member.js +84 -0
  133. package/dist/rooms-tasks/archived-absence.d.ts +4 -0
  134. package/dist/rooms-tasks/archived-absence.js +11 -0
  135. package/dist/rooms-tasks/cli.js +182 -5
  136. package/dist/rooms-tasks/close.d.ts +5 -2
  137. package/dist/rooms-tasks/close.js +224 -36
  138. package/dist/rooms-tasks/config.d.ts +1 -1
  139. package/dist/rooms-tasks/config.js +11 -4
  140. package/dist/rooms-tasks/cowork-adapter.d.ts +12 -5
  141. package/dist/rooms-tasks/cowork-adapter.js +15 -121
  142. package/dist/rooms-tasks/cowork-http.js +56 -6
  143. package/dist/rooms-tasks/deletion.d.ts +1 -1
  144. package/dist/rooms-tasks/deletion.js +66 -10
  145. package/dist/rooms-tasks/erasure.d.ts +6 -0
  146. package/dist/rooms-tasks/erasure.js +173 -0
  147. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  148. package/dist/rooms-tasks/external-worker.js +3 -4
  149. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  150. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  151. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  152. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  153. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  154. package/dist/rooms-tasks/layout-cli.js +107 -0
  155. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  156. package/dist/rooms-tasks/layout-config.js +89 -0
  157. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  158. package/dist/rooms-tasks/layout-control.js +52 -0
  159. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  160. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  161. package/dist/rooms-tasks/layout-service.d.ts +43 -0
  162. package/dist/rooms-tasks/layout-service.js +203 -0
  163. package/dist/rooms-tasks/layout.d.ts +121 -0
  164. package/dist/rooms-tasks/layout.js +286 -0
  165. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  166. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  167. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  168. package/dist/rooms-tasks/live-readiness.js +85 -0
  169. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  170. package/dist/rooms-tasks/member-ownership.js +40 -0
  171. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  172. package/dist/rooms-tasks/member-startup.js +2 -0
  173. package/dist/rooms-tasks/provision.d.ts +3 -0
  174. package/dist/rooms-tasks/provision.js +101 -49
  175. package/dist/rooms-tasks/recovery.d.ts +19 -0
  176. package/dist/rooms-tasks/recovery.js +110 -0
  177. package/dist/rooms-tasks/room-state.d.ts +4 -2
  178. package/dist/rooms-tasks/room-state.js +47 -8
  179. package/dist/rooms-tasks/task-review.d.ts +113 -0
  180. package/dist/rooms-tasks/task-review.js +561 -0
  181. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  182. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  183. package/dist/rooms-tasks/task-state.d.ts +51 -5
  184. package/dist/rooms-tasks/task-state.js +358 -50
  185. package/dist/rooms-tasks/task-tools.d.ts +35 -0
  186. package/dist/rooms-tasks/task-tools.js +134 -0
  187. package/dist/rooms-tasks/types.d.ts +78 -0
  188. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  189. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  190. package/dist/rooms-tasks/workspace.d.ts +35 -0
  191. package/dist/rooms-tasks/workspace.js +325 -0
  192. package/dist/runner.d.ts +23 -4
  193. package/dist/runner.js +409 -61
  194. package/dist/session/acp.d.ts +75 -2
  195. package/dist/session/acp.js +345 -82
  196. package/dist/session/arbiter.d.ts +98 -0
  197. package/dist/session/arbiter.js +272 -3
  198. package/dist/session/codex-app-server.d.ts +45 -1
  199. package/dist/session/codex-app-server.js +225 -32
  200. package/dist/session/control.d.ts +17 -3
  201. package/dist/session/control.js +219 -23
  202. package/dist/session/conversation-normalizer.d.ts +2 -1
  203. package/dist/session/conversation-normalizer.js +1 -1
  204. package/dist/session/conversation-observation.d.ts +25 -0
  205. package/dist/session/conversation-observation.js +100 -0
  206. package/dist/session/conversation-replay.d.ts +50 -0
  207. package/dist/session/conversation-replay.js +203 -0
  208. package/dist/session/conversation-store.d.ts +15 -1
  209. package/dist/session/conversation-store.js +213 -26
  210. package/dist/session/conversation-types.d.ts +57 -4
  211. package/dist/session/prompt-batch.d.ts +79 -0
  212. package/dist/session/prompt-batch.js +120 -0
  213. package/dist/session/task-notice.d.ts +14 -0
  214. package/dist/session/task-notice.js +13 -0
  215. package/dist/session/types.d.ts +42 -1
  216. package/dist/setup-tunnel-preflight.d.ts +34 -0
  217. package/dist/setup-tunnel-preflight.js +62 -0
  218. package/dist/socket-path.d.ts +2 -0
  219. package/dist/socket-path.js +22 -0
  220. package/dist/spawn.d.ts +5 -1
  221. package/dist/spawn.js +40 -4
  222. package/dist/startup-prompt.d.ts +15 -0
  223. package/dist/startup-prompt.js +71 -0
  224. package/dist/startup-readiness.d.ts +20 -1
  225. package/dist/startup-readiness.js +66 -3
  226. package/dist/startup-telemetry.d.ts +16 -0
  227. package/dist/startup-telemetry.js +46 -0
  228. package/dist/subscriptions/cli.d.ts +91 -0
  229. package/dist/subscriptions/cli.js +223 -0
  230. package/dist/subscriptions/launch.d.ts +37 -0
  231. package/dist/subscriptions/launch.js +93 -0
  232. package/dist/subscriptions/login.d.ts +77 -0
  233. package/dist/subscriptions/login.js +215 -0
  234. package/dist/subscriptions/observed.d.ts +7 -0
  235. package/dist/subscriptions/observed.js +44 -0
  236. package/dist/subscriptions/probe.d.ts +10 -0
  237. package/dist/subscriptions/probe.js +73 -0
  238. package/dist/subscriptions/service.d.ts +102 -0
  239. package/dist/subscriptions/service.js +364 -0
  240. package/dist/subscriptions/store.d.ts +71 -0
  241. package/dist/subscriptions/store.js +143 -0
  242. package/dist/supervisor/adoption.d.ts +8 -0
  243. package/dist/supervisor/adoption.js +78 -0
  244. package/dist/supervisor/catalog.d.ts +62 -0
  245. package/dist/supervisor/catalog.js +254 -0
  246. package/dist/supervisor/fleet.d.ts +8 -0
  247. package/dist/supervisor/fleet.js +89 -0
  248. package/dist/supervisor/index.js +3 -6
  249. package/dist/supervisor/launchd.d.ts +1 -1
  250. package/dist/supervisor/launchd.js +26 -13
  251. package/dist/supervisor/legacy.d.ts +35 -0
  252. package/dist/supervisor/legacy.js +524 -0
  253. package/dist/supervisor/manager.d.ts +15 -0
  254. package/dist/supervisor/manager.js +270 -0
  255. package/dist/supervisor/registration-refresh.d.ts +45 -0
  256. package/dist/supervisor/registration-refresh.js +266 -0
  257. package/dist/supervisor/scope.d.ts +4 -0
  258. package/dist/supervisor/scope.js +10 -0
  259. package/dist/supervisor/systemd.d.ts +2 -2
  260. package/dist/supervisor/systemd.js +18 -37
  261. package/dist/task-supervision.d.ts +9 -0
  262. package/dist/task-supervision.js +145 -0
  263. package/dist/temp-idle.d.ts +17 -0
  264. package/dist/temp-idle.js +42 -0
  265. package/dist/temp-lifecycle.d.ts +23 -8
  266. package/dist/temp-lifecycle.js +123 -66
  267. package/dist/temp-supervisor-recovery.d.ts +1 -1
  268. package/dist/temp-supervisor-recovery.js +2 -2
  269. package/dist/tunnel-setup-completion.d.ts +7 -0
  270. package/dist/tunnel-setup-completion.js +13 -0
  271. package/dist/tunnel-setup-input.d.ts +19 -0
  272. package/dist/tunnel-setup-input.js +130 -0
  273. package/dist/watchdog/query.d.ts +3 -1
  274. package/dist/watchdog/query.js +2 -1
  275. package/dist/web/agent-attachments.d.ts +29 -0
  276. package/dist/web/agent-attachments.js +162 -0
  277. package/dist/web/auth.d.ts +65 -2
  278. package/dist/web/auth.js +201 -11
  279. package/dist/web/available-port.d.ts +2 -0
  280. package/dist/web/available-port.js +23 -0
  281. package/dist/web/control.d.ts +3 -3
  282. package/dist/web/control.js +10 -8
  283. package/dist/web/conversation-stream-codec.d.ts +23 -0
  284. package/dist/web/conversation-stream-codec.js +110 -0
  285. package/dist/web/conversation-stream.d.ts +26 -0
  286. package/dist/web/conversation-stream.js +239 -0
  287. package/dist/web/events.d.ts +2 -0
  288. package/dist/web/events.js +9 -0
  289. package/dist/web/fleet-config-service.d.ts +7 -1
  290. package/dist/web/fleet-config-service.js +61 -16
  291. package/dist/web/prefix-gateway.d.ts +32 -0
  292. package/dist/web/prefix-gateway.js +248 -0
  293. package/dist/web/resource-views.d.ts +138 -0
  294. package/dist/web/resource-views.js +519 -0
  295. package/dist/web/runtime.d.ts +3 -1
  296. package/dist/web/runtime.js +132 -34
  297. package/dist/web/server.d.ts +19 -0
  298. package/dist/web/server.js +893 -71
  299. package/dist/web/service.d.ts +11 -2
  300. package/dist/web/service.js +30 -4
  301. package/dist/web/workspace-devices.d.ts +107 -0
  302. package/dist/web/workspace-devices.js +277 -0
  303. package/dist/web/workspace-tunnel.d.ts +1 -0
  304. package/dist/web/workspace-tunnel.js +34 -0
  305. package/dist/workspace-enrollment.d.ts +83 -0
  306. package/dist/workspace-enrollment.js +605 -0
  307. package/dist/workspace-replacement.d.ts +66 -0
  308. package/dist/workspace-replacement.js +238 -0
  309. package/docs/acp-files.md +146 -0
  310. package/docs/conversation-stream.md +216 -0
  311. package/docs/fleet-migration.md +54 -0
  312. package/docs/live-voice.md +13 -0
  313. package/docs/room-layouts.md +227 -0
  314. package/docs/self-reminders.md +374 -0
  315. package/docs/task-progress-review.md +143 -0
  316. package/docs/task-workspaces.md +225 -0
  317. package/docs/validation/managed-cli-permissions.md +421 -0
  318. package/examples/room-layouts/fresh.yaml +27 -0
  319. package/examples/room-layouts/scoped.yaml +21 -0
  320. package/examples/room-layouts/shared.yaml +19 -0
  321. package/package.json +25 -20
  322. package/presets/README.md +78 -0
  323. package/presets/brain-catalog.json +8 -2
  324. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  325. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  326. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  327. package/presets/fleet/agent_templates/LocalCoordinator.yaml +6 -3
  328. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  329. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  330. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  331. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  332. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  333. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  334. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  335. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  336. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  337. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  338. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  339. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  340. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  341. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  342. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  343. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  344. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  345. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  346. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  347. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  348. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  349. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  350. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  351. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  352. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  353. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  354. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  355. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  356. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  357. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  358. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  359. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  360. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  361. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  362. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  363. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  364. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  365. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  366. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  367. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  368. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  369. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  370. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  371. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  372. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  373. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  374. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  375. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  376. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  377. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  378. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  379. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  380. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  381. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  382. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  383. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  384. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  385. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  386. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  387. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  388. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  389. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  390. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  391. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  392. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  393. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  394. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  395. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  396. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  397. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  398. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  399. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  400. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  401. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  402. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  403. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  404. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  405. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  406. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  407. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  408. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  409. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  410. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  411. package/presets/fleet/roles/Coordinator.yaml +21 -13
  412. package/presets/fleet/roles/Critic.yaml +24 -0
  413. package/presets/fleet/roles/Developer.yaml +32 -0
  414. package/presets/fleet/roles/Engineer.yaml +72 -0
  415. package/presets/fleet/roles/LocalCoordinator.yaml +17 -0
  416. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  417. package/presets/fleet/room_layouts/pair.yaml +24 -0
  418. package/presets/fleet/room_layouts/single.yaml +17 -0
  419. package/presets/fleet/room_layouts/team.yaml +30 -0
  420. package/presets/fleet/room_templates/engineering.yaml +19 -0
  421. package/presets/manifest.json +2 -2
  422. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  423. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  424. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  425. package/dist/web-app/icons/ours-fleet.svg +0 -4
  426. package/dist/web-app/index.html +0 -17
  427. package/dist/web-app/manifest.webmanifest +0 -15
  428. package/dist/web-app/offline.html +0 -18
  429. package/dist/web-app/sw.js +0 -51
@@ -1,8 +1,15 @@
1
1
  import type { ArchivedMemberAbsence } from './types.js';
2
- import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
2
+ import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase, TaskNotificationEvent } from './types.js';
3
3
  export declare const tasksDir: () => string;
4
4
  export declare class TaskStateError extends Error {
5
5
  }
6
+ /** Atomic snapshots contain the events, including intermediate states between polls. */
7
+ export declare function pendingTaskNotifications(warn?: (line: string) => void): Array<{
8
+ taskId: string;
9
+ events: TaskNotificationEvent[];
10
+ }>;
11
+ /** Service event-id dedupe covers acceptance followed by a crash before this acknowledgement. */
12
+ export declare function acknowledgeTaskNotification(id: string, eventId: string): void;
6
13
  /**
7
14
  * Accepted deletion is the per-task epoch guard: once pending, every lifecycle
8
15
  * mutation, terminal settlement, and room publication must fail boundedly.
@@ -18,6 +25,10 @@ export interface CreateTaskInput {
18
25
  idempotency_key?: string;
19
26
  start?: boolean;
20
27
  no_room?: boolean;
28
+ layout?: {
29
+ name: string;
30
+ definition_hash: string;
31
+ };
21
32
  room_id?: string;
22
33
  listId?: string;
23
34
  }
@@ -28,6 +39,7 @@ export declare function listTasks(filter?: {
28
39
  state?: TaskState | TaskState[];
29
40
  listId?: string;
30
41
  includeDeleting?: boolean;
42
+ strict?: boolean;
31
43
  }): TaskRecord[];
32
44
  export declare function moveTaskToList(id: string, listId: string): TaskRecord;
33
45
  export declare function findByIdempotencyKey(key: string): TaskRecord | undefined;
@@ -62,9 +74,8 @@ export declare function getDeletingTask(id: string): TaskRecord;
62
74
  export declare function taskDeletionState(id: string): 'none' | 'pending' | 'absent';
63
75
  /**
64
76
  * Durable, surface-independent audit evidence for a permanent deletion. The
65
- * receipt outlives the task record: written with the acceptance intent, and
66
- * completed before settlement is reported. Metadata only — never brief or
67
- * room content.
77
+ * receipt supports in-flight recovery only: written with acceptance intent,
78
+ * then erased before settlement is reported. It may contain a bounded title.
68
79
  */
69
80
  export interface TaskDeletionReceipt {
70
81
  schema_version: 1;
@@ -88,7 +99,7 @@ export declare function readTaskDeletionReceipt(id: string): TaskDeletionReceipt
88
99
  * settlement rather than deleting resources without audit evidence.
89
100
  */
90
101
  export declare function ensureTaskDeletionReceipt(id: string): void;
91
- /** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
102
+ /** Erase the in-flight receipt; also heals a crash after task unlink. */
92
103
  export declare function completeTaskDeletionReceipt(id: string): void;
93
104
  export type TaskDeletionAcceptance = {
94
105
  status: 'accepted' | 'pending';
@@ -127,8 +138,10 @@ interface SeatEvidence {
127
138
  launch?: {
128
139
  launch_id?: string;
129
140
  action_id?: string;
141
+ task_supervised?: boolean;
130
142
  };
131
143
  retirement?: {
144
+ absence_verified?: boolean;
132
145
  phase: TaskDeletionMemberPhase;
133
146
  launch_id: string;
134
147
  archive_path?: string;
@@ -156,10 +169,43 @@ export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: Re
156
169
  * never-launched proof or a freshly verified archived-absence checkpoint.
157
170
  */
158
171
  export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>, proofs?: ReadonlyArray<ArchivedMemberAbsence>): TaskRecord;
172
+ /** Settlement-only checkpoint, under the task-operation lock after fresh archive verification.
173
+ * From here retries still verify live absence, but owned archives may be consumed.
174
+ */
175
+ export declare function beginTaskWorkspaceCleanup(id: string): void;
159
176
  /**
160
177
  * Physically remove a deletion-pending task record. Settlement-only: callers
161
178
  * must have completed member retirement and room cleanup first. Missing
162
179
  * records are the idempotent already-settled outcome.
163
180
  */
164
181
  export declare function unlinkDeletedTask(id: string): boolean;
182
+ /** Called only after current temp and exact identity absence have been checked. */
183
+ export declare function settleAbsentTaskDeletionMember(id: string, name: string): void;
184
+ /** Room-only erasure keeps the parent Task but removes its dead room/member links. */
185
+ export declare function detachDeletedRoom(taskId: string, roomId: string): void;
186
+ /** A completed room may consume its archives while other task members still retire. */
187
+ export declare function beginTaskArchiveCleanup(id: string): void;
188
+ /** Metadata-only edit; compare under the task lock to prevent lost updates. */
189
+ /**
190
+ * Choose, change or clear a Backlog task's Room Layout before it starts.
191
+ * `expected` is the layout name the caller saw (null = none); a different
192
+ * current choice is a stale edit. Choosing a layout drops the sealed room
193
+ * template plan; the caller releases the returned launch snapshot hash.
194
+ */
195
+ export declare function updateTaskLayout(id: string, expected: string | null, next: {
196
+ name: string;
197
+ definition_hash: string;
198
+ } | null): {
199
+ task: TaskRecord;
200
+ releasedSnapshot?: string;
201
+ };
202
+ /**
203
+ * Progress and To review edits: one read-modify-write under the task lock, so
204
+ * concurrent writers never lose each other's changes. Lifecycle state is untouched.
205
+ */
206
+ export declare function updateTaskContribution<T>(id: string, change: (task: TaskRecord) => {
207
+ value: T;
208
+ changed: boolean;
209
+ }): T;
210
+ export declare function updateTaskBrief(id: string, brief: string, expectedBrief: string): TaskRecord;
165
211
  export {};
@@ -1,64 +1,136 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, rmSync, renameSync } from 'node:fs';
1
+ import { publishFleetResourceCommit } from '../application/resource-observer.js';
2
+ import { existsSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, rmSync, renameSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
4
- import { replaceFileAtomically } from '../atomic-file.js';
5
+ import { replaceFileAtomically, withSynchronousFileLock } from '../atomic-file.js';
6
+ import { incarnationEnded, processIncarnation } from '../process-incarnation.js';
5
7
  import { stateRoot } from '../paths.js';
8
+ import { planWorkspace, ensureWorkspace } from './workspace.js';
6
9
  import { storedRoomLaunchPolicy, TASK_TERMINAL_STATES, TASK_CANCELLABLE_STATES } from './types.js';
7
10
  import { DEFAULT_TASK_LIST_ID, readTaskLists } from './task-lists.js';
8
11
  export const tasksDir = () => join(stateRoot(), 'tasks');
9
12
  function taskPath(id) { return join(tasksDir(), `${id}.json`); }
10
13
  function taskLockPath(id) { return join(tasksDir(), '.locks', id); }
11
14
  const localTaskLocks = new Set();
15
+ /**
16
+ * Remove a lock atomically: rename it to a unique name first, so no other
17
+ * process ever observes a half-removed lock.
18
+ */
19
+ function retireLock(path) {
20
+ const retired = `${path}.retired.${process.pid}.${randomUUID()}`;
21
+ try {
22
+ renameSync(path, retired);
23
+ }
24
+ catch (error) {
25
+ if (error.code === 'ENOENT')
26
+ return;
27
+ throw error;
28
+ }
29
+ rmSync(retired, { recursive: true, force: true });
30
+ }
31
+ /** A legacy lock directory without an owner file younger than this may still be releasing. */
32
+ const LEGACY_OWNERLESS_LOCK_GRACE_MS = 5_000;
33
+ /**
34
+ * Hold a task's cross-process lock while `fn` runs.
35
+ *
36
+ * The lock is a file holding its owner record, published complete by hard-linking
37
+ * a written temporary file: link never replaces an existing lock, and there is no
38
+ * moment at which a live claimer has an ownerless lock that could look abandoned.
39
+ * A lock is reclaimed only when its owner process has provably ended; reclaimers
40
+ * take turns and remove only the lock they observed. Directory locks are left by
41
+ * earlier versions; they are honored, and an ownerless one is reclaimed once it is
42
+ * old enough, since this version never creates one.
43
+ */
12
44
  function withTaskLock(id, fn) {
13
45
  if (localTaskLocks.has(id))
14
46
  return fn();
15
47
  const path = taskLockPath(id);
16
- const ownerPath = join(path, 'owner.json');
17
48
  const token = randomUUID();
18
49
  mkdirSync(join(tasksDir(), '.locks'), { recursive: true });
19
- for (;;) {
20
- const claimPath = `${path}.claim.${process.pid}.${randomUUID()}`;
50
+ const readOwner = () => {
21
51
  try {
22
- mkdirSync(claimPath);
23
- writeFileSync(join(claimPath, 'owner.json'), JSON.stringify({ token, pid: process.pid }));
24
- renameSync(claimPath, path);
25
- break;
52
+ return JSON.parse(readFileSync(path, 'utf8'));
26
53
  }
27
54
  catch (error) {
28
- rmSync(claimPath, { recursive: true, force: true });
29
- if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
30
- throw error;
55
+ if (error.code !== 'EISDIR')
56
+ return undefined;
57
+ try {
58
+ return JSON.parse(readFileSync(join(path, 'owner.json'), 'utf8'));
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ };
65
+ /** Who holds the lock now, and whether it can be proved abandoned. */
66
+ const holder = () => {
67
+ let stat;
68
+ try {
69
+ stat = lstatSync(path);
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ const owner = readOwner();
75
+ if (owner && typeof owner.pid === 'number') {
31
76
  let alive = false;
32
77
  try {
33
- const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
34
- if (typeof owner.pid === 'number') {
35
- try {
36
- process.kill(owner.pid, 0);
37
- alive = true;
38
- }
39
- catch { /* dead */ }
40
- }
78
+ process.kill(owner.pid, 0);
79
+ alive = true;
80
+ }
81
+ catch (error) {
82
+ alive = error.code !== 'ESRCH';
83
+ }
84
+ return { token: owner.token, stale: !alive || incarnationEnded(owner.pid, owner) };
85
+ }
86
+ return {
87
+ token: undefined,
88
+ stale: stat.isDirectory() && Date.now() - stat.mtimeMs > LEGACY_OWNERLESS_LOCK_GRACE_MS,
89
+ };
90
+ };
91
+ const claimPath = `${path}.claim.${process.pid}.${randomUUID()}`;
92
+ writeFileSync(claimPath, JSON.stringify({ token, pid: process.pid, ...processIncarnation(process.pid) }));
93
+ try {
94
+ for (;;) {
95
+ try {
96
+ linkSync(claimPath, path);
97
+ break;
41
98
  }
42
- catch { /* corrupt legacy lock */ }
43
- if (!alive) {
44
- rmSync(path, { recursive: true, force: true });
99
+ catch (error) {
100
+ if (error.code !== 'EEXIST')
101
+ throw error;
102
+ }
103
+ const observed = holder();
104
+ if (observed?.stale) {
105
+ try {
106
+ withSynchronousFileLock(`${path}.reclaim`, () => {
107
+ const current = holder();
108
+ if (current?.stale && current.token === observed.token)
109
+ retireLock(path);
110
+ });
111
+ }
112
+ catch (error) {
113
+ // A contender stalled inside the reclaim section: keep waiting like any other poll.
114
+ if (!(error instanceof Error) || !/^Timed out acquiring synchronous/.test(error.message))
115
+ throw error;
116
+ }
45
117
  continue;
46
118
  }
47
119
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
48
120
  }
49
121
  }
122
+ finally {
123
+ rmSync(claimPath, { force: true });
124
+ }
50
125
  localTaskLocks.add(id);
51
126
  try {
52
127
  return fn();
53
128
  }
54
129
  finally {
55
130
  localTaskLocks.delete(id);
56
- try {
57
- const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
58
- if (owner.token === token)
59
- rmSync(path, { recursive: true, force: true });
60
- }
61
- catch { /* ownership cannot be proved */ }
131
+ // Only this live owner removes its lock; reclaimers never take a live owner's lock.
132
+ if (readOwner()?.token === token)
133
+ retireLock(path);
62
134
  }
63
135
  }
64
136
  function generateTaskId() {
@@ -88,7 +160,88 @@ function writeTask(record) {
88
160
  mkdirSync(tasksDir(), { recursive: true });
89
161
  const persisted = { ...record };
90
162
  delete persisted.list_name;
163
+ const path = taskPath(record.task_id);
164
+ const previous = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
165
+ const status = (task) => task.deletion?.status === 'pending' ? 'deleting' : task.blocked ? `${task.state} (blocked)` : task.state;
166
+ // Read pending events under the same task lock as every mutation. Metadata edits
167
+ // and retries preserve the queue; acknowledgement cannot be resurrected by stale input.
168
+ const pending = [...(previous?.notification_events ?? [])];
169
+ if (!previous || status(previous) !== status(record)) {
170
+ const next = status(record), before = previous ? status(previous) : undefined;
171
+ pending.push({ eventId: `task:${record.task_id}:${randomUUID()}`, taskId: record.task_id,
172
+ title: `Task ${!previous ? 'created' : 'updated'}: ${record.title}`.slice(0, 160),
173
+ body: `Task ${record.task_id}: ${before ? `${before} → ` : 'created in '}${next}.`,
174
+ target: { kind: 'task', taskId: record.task_id } });
175
+ }
176
+ if (pending.length)
177
+ persisted.notification_events = pending;
178
+ else
179
+ delete persisted.notification_events;
91
180
  replaceFileAtomically(taskPath(record.task_id), JSON.stringify(persisted, null, 2) + '\n');
181
+ publishFleetResourceCommit(taskPath(record.task_id));
182
+ }
183
+ const retiredNotificationsDir = () => join(tasksDir(), '.notification-retired');
184
+ function typedTaskNotification(value, id) {
185
+ if (!value || typeof value !== 'object' || Object.keys(value).some(key => !['eventId', 'title', 'body', 'target', 'taskId'].includes(key)))
186
+ return false;
187
+ const target = value.target;
188
+ return typeof value.eventId === 'string' && value.eventId.startsWith(`task:${id}:`) && value.eventId.length <= 512
189
+ && value.taskId === id && !!target && typeof target === 'object' && !Array.isArray(target)
190
+ && Object.keys(target).every(key => ['kind', 'taskId'].includes(key)) && target.taskId === id
191
+ && (target.kind === 'task' || (target.kind === 'task-deleted' && value.eventId === `task:${id}:deleted`))
192
+ && typeof value.title === 'string' && !!value.title.trim() && value.title.length <= 160
193
+ && typeof value.body === 'string' && !!value.body.trim() && value.body.length <= 512;
194
+ }
195
+ /** Atomic snapshots contain the events, including intermediate states between polls. */
196
+ export function pendingTaskNotifications(warn = () => { }) {
197
+ const result = [];
198
+ for (const dir of [tasksDir(), retiredNotificationsDir()]) {
199
+ if (!existsSync(dir))
200
+ continue;
201
+ for (const file of readdirSync(dir).filter(file => TASK_ID_PATTERN.test(file.slice(0, -5)) && file.endsWith('.json'))) {
202
+ try {
203
+ const path = join(dir, file), stat = lstatSync(path);
204
+ if (!stat.isFile() || stat.isSymbolicLink())
205
+ throw new Error('invalid notification source');
206
+ const value = JSON.parse(readFileSync(path, 'utf8'));
207
+ const id = file.slice(0, -5);
208
+ // A prepared deletion copy has no authority while the original still exists.
209
+ if (dir === retiredNotificationsDir() && existsSync(taskPath(id)))
210
+ continue;
211
+ if (value.notification_events === undefined)
212
+ continue;
213
+ if (!Array.isArray(value.notification_events))
214
+ throw new Error('invalid notification events');
215
+ // Only typed task events are delivered. Unsupported entries are never sent or converted,
216
+ // and they do not block later lifecycle events.
217
+ const events = value.notification_events.filter(event => typedTaskNotification(event, id));
218
+ if (events.length)
219
+ result.push({ taskId: id, events });
220
+ }
221
+ catch (error) {
222
+ if (!isNotFoundError(error))
223
+ warn('Task notification record unavailable; other task deliveries continue');
224
+ }
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ /** Service event-id dedupe covers acceptance followed by a crash before this acknowledgement. */
230
+ export function acknowledgeTaskNotification(id, eventId) {
231
+ assertCanonicalTaskId(id);
232
+ withTaskLock(id, () => {
233
+ for (const path of [taskPath(id), join(retiredNotificationsDir(), `${id}.json`)]) {
234
+ if (!existsSync(path))
235
+ continue;
236
+ const value = JSON.parse(readFileSync(path, 'utf8'));
237
+ if (!value.notification_events?.some(event => typedTaskNotification(event, id) && event.eventId === eventId))
238
+ continue;
239
+ value.notification_events = value.notification_events.filter(event => !typedTaskNotification(event, id) || event.eventId !== eventId);
240
+ replaceFileAtomically(path, JSON.stringify(value, null, 2) + '\n');
241
+ if (path.startsWith(retiredNotificationsDir() + '/') && !value.notification_events.length)
242
+ unlinkSync(path);
243
+ }
244
+ });
92
245
  }
93
246
  function presentTask(record) {
94
247
  const listId = record.list_id ?? DEFAULT_TASK_LIST_ID;
@@ -125,21 +278,32 @@ export function assertNoPendingDeletion(task) {
125
278
  throw new TaskStateError(`task ${task.task_id} is pending deletion; run 'ours-fleet task delete ${task.task_id} ${task.task_id}' to retry cleanup`);
126
279
  }
127
280
  }
281
+ function creationPlan(plan, layout) {
282
+ return JSON.stringify([plan?.plan_hash ?? null, storedRoomLaunchPolicy(plan?.room_policy) ?? null,
283
+ layout ? `${layout.name}@${layout.definition_hash}` : null]);
284
+ }
128
285
  export function createTask(input) {
129
286
  const key = input.idempotency_key ?? randomUUID();
130
- const existing = findByIdempotencyKey(key);
287
+ const existing = listTasks({ includeDeleting: true }).find(task => task.idempotency_key === key);
288
+ const requestedPlan = creationPlan(input.execution_plan, input.layout);
131
289
  if (existing) {
132
- const existingPlan = existing.execution_plan?.plan_hash;
133
- const requestedPlan = input.execution_plan?.plan_hash;
134
- const existingPolicy = storedRoomLaunchPolicy(existing.execution_plan?.room_policy);
135
- const requestedPolicy = storedRoomLaunchPolicy(input.execution_plan?.room_policy);
136
- if (existingPlan !== requestedPlan || JSON.stringify(existingPolicy) !== JSON.stringify(requestedPolicy))
290
+ // After a Backlog plan edit the current plan differs from the one created; retries compare the original request.
291
+ if ((existing.creation_plan ?? creationPlan(existing.execution_plan, existing.layout)) !== requestedPlan)
137
292
  throw new TaskStateError(`idempotency key '${key}' was already used with a different execution plan`);
138
- return existing;
293
+ return withTaskLock(existing.task_id, () => {
294
+ const fresh = readTask(existing.task_id);
295
+ assertNoPendingDeletion(fresh);
296
+ if (fresh.workspace)
297
+ ensureWorkspace(fresh.workspace);
298
+ return fresh;
299
+ });
139
300
  }
301
+ if (input.layout && (input.execution_plan || input.no_room || input.room_id))
302
+ throw new TaskStateError('a layout task cannot also use a room template, no-room mode or a room');
140
303
  const state = input.start === false ? 'backlog' : 'provisioning';
304
+ const taskId = generateTaskId();
141
305
  const record = {
142
- task_id: generateTaskId(),
306
+ task_id: taskId,
143
307
  list_id: input.listId ?? DEFAULT_TASK_LIST_ID,
144
308
  title: input.title,
145
309
  brief: input.brief,
@@ -148,14 +312,18 @@ export function createTask(input) {
148
312
  template: input.template,
149
313
  execution_plan: input.execution_plan,
150
314
  no_room: input.no_room || undefined,
315
+ ...(input.layout ? { layout: { name: input.layout.name, run_id: `task-${taskId}`, definition_hash: input.layout.definition_hash } } : {}),
151
316
  room_id: input.room_id,
152
317
  member_roles: [],
153
318
  origin: input.origin,
154
319
  idempotency_key: key,
320
+ creation_plan: requestedPlan,
155
321
  created_at: new Date().toISOString(),
156
322
  started_at: state === 'provisioning' ? new Date().toISOString() : undefined,
157
323
  };
324
+ record.workspace = planWorkspace('task', record.task_id);
158
325
  writeTask(record);
326
+ ensureWorkspace(record.workspace);
159
327
  return readTask(record.task_id);
160
328
  }
161
329
  export function getTask(id) { return withTaskLock(id, () => readTask(id)); }
@@ -195,7 +363,10 @@ export function listTasks(filter) {
195
363
  if ((!states || states.includes(t.state)) && (!filter?.listId || t.list_id === filter.listId))
196
364
  tasks.push(t);
197
365
  }
198
- catch { /* corrupt file, skip */ }
366
+ catch (error) {
367
+ if (filter?.strict)
368
+ throw error; /* legacy CLI inventory tolerates corrupt files */
369
+ }
199
370
  }
200
371
  const order = new Map(readTaskLists().map((item, index) => [item.list_id, index]));
201
372
  return tasks.sort((a, b) => (order.get(a.list_id) - order.get(b.list_id))
@@ -526,14 +697,10 @@ export function ensureTaskDeletionReceipt(id) {
526
697
  writeDeletionReceiptForIntent(stored);
527
698
  });
528
699
  }
529
- /** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
700
+ /** Erase the in-flight receipt; also heals a crash after task unlink. */
530
701
  export function completeTaskDeletionReceipt(id) {
531
- const receipt = readTaskDeletionReceipt(id);
532
- if (!receipt || receipt.settled_at)
533
- return;
534
- receipt.settled_at = new Date().toISOString();
535
- receipt.result = 'deleted';
536
- replaceFileAtomically(deletionReceiptPath(id), JSON.stringify(receipt, null, 2) + '\n');
702
+ assertCanonicalTaskId(id);
703
+ rmSync(deletionReceiptPath(id), { force: true });
537
704
  }
538
705
  /**
539
706
  * Persist the first-wins durable deletion intent. Accepts every lifecycle
@@ -552,8 +719,10 @@ export function beginTaskDeletionIntent(id, actor) {
552
719
  stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
553
720
  }
554
721
  catch (error) {
555
- if (isNotFoundError(error))
722
+ if (isNotFoundError(error)) {
723
+ completeTaskDeletionReceipt(id);
556
724
  return { status: 'already_absent' };
725
+ }
557
726
  throw error;
558
727
  }
559
728
  if (stored.deletion?.status === 'pending') {
@@ -650,6 +819,8 @@ function findCursorForSeat(id, stored, seat) {
650
819
  if (cursor && seat.identity_cid
651
820
  && cursor.identity_cid.toLowerCase() !== seat.identity_cid.toLowerCase())
652
821
  throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' identity CID mismatch between cursor and room seat`);
822
+ if (cursor?.action_id && seat.launch?.action_id && cursor.action_id !== seat.launch.action_id)
823
+ throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' creation ownership proof changed`);
653
824
  return cursor;
654
825
  }
655
826
  /**
@@ -670,11 +841,20 @@ export function upsertTaskDeletionMembersFromSeats(id, seats) {
670
841
  for (const seat of seats) {
671
842
  if (!seat.identity_cid)
672
843
  continue;
673
- if (!findCursorForSeat(id, stored, seat)) {
844
+ const cursor = findCursorForSeat(id, stored, seat);
845
+ if (!cursor) {
674
846
  stored.deletion.members.push({
675
847
  name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now,
848
+ ...(seat.launch?.action_id ? { action_id: seat.launch.action_id } : {}),
849
+ ...(seat.launch?.task_supervised ? { task_supervised: true } : {}),
676
850
  });
677
851
  }
852
+ else {
853
+ if (seat.launch?.action_id)
854
+ cursor.action_id ??= seat.launch.action_id;
855
+ if (seat.launch?.task_supervised)
856
+ cursor.task_supervised = true;
857
+ }
678
858
  }
679
859
  writeTask(stored);
680
860
  return presentTaskLenient(stored);
@@ -703,12 +883,17 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
703
883
  const evidence = seat.retirement;
704
884
  if (evidence?.phase !== 'identity_absent')
705
885
  throw new TaskStateError(`task ${id} deletion cannot checkpoint member '${seat.role_name}' before completed retirement`);
706
- const neverLaunched = evidence.launch_id === DELETION_MEMBER_NEVER_LAUNCHED;
886
+ const neverLaunched = evidence.launch_id === DELETION_MEMBER_NEVER_LAUNCHED
887
+ || evidence.launch_id === DELETION_MEMBER_ABSENT_VERIFIED || evidence.absence_verified === true;
707
888
  if (!neverLaunched && evidence.archive_path === undefined)
708
889
  throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' retirement evidence lacks its archive`);
709
890
  if (!seat.identity_cid) {
710
- if (neverLaunched)
711
- continue; // provably never held a managed identity
891
+ if (neverLaunched) {
892
+ stored.deletion.absent_members ??= [];
893
+ if (!stored.deletion.absent_members.includes(seat.role_name))
894
+ stored.deletion.absent_members.push(seat.role_name);
895
+ continue;
896
+ }
712
897
  const proof = proofs.find(value => value.name === seat.role_name);
713
898
  if (proof && proof.launch_id === evidence.launch_id && proof.archive_path === evidence.archive_path
714
899
  && proof.launch_id === seat.launch?.launch_id && proof.action_id === seat.launch?.action_id) {
@@ -728,6 +913,10 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
728
913
  cursor = { name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now };
729
914
  stored.deletion.members.push(cursor);
730
915
  }
916
+ if (seat.launch?.action_id)
917
+ cursor.action_id ??= seat.launch.action_id;
918
+ if (seat.launch?.task_supervised)
919
+ cursor.task_supervised = true;
731
920
  if (DELETION_MEMBER_PHASE_ORDER[evidence.phase] < DELETION_MEMBER_PHASE_ORDER[cursor.phase])
732
921
  continue;
733
922
  if (cursor.launch_id !== undefined && cursor.launch_id !== evidence.launch_id)
@@ -746,6 +935,19 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
746
935
  return presentTaskLenient(stored);
747
936
  });
748
937
  }
938
+ /** Settlement-only checkpoint, under the task-operation lock after fresh archive verification.
939
+ * From here retries still verify live absence, but owned archives may be consumed.
940
+ */
941
+ export function beginTaskWorkspaceCleanup(id) {
942
+ withTaskLock(id, () => {
943
+ const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
944
+ if (stored.deletion?.status !== 'pending'
945
+ || stored.deletion.members.some(member => member.phase !== 'identity_absent'))
946
+ throw new TaskStateError('Task is not ready for workspace cleanup');
947
+ stored.deletion.workspace_cleanup_started_at ??= new Date().toISOString();
948
+ writeTask(stored);
949
+ });
950
+ }
749
951
  /**
750
952
  * Physically remove a deletion-pending task record. Settlement-only: callers
751
953
  * must have completed member retirement and room cleanup first. Missing
@@ -765,6 +967,12 @@ export function unlinkDeletedTask(id) {
765
967
  }
766
968
  if (stored.deletion?.status !== 'pending')
767
969
  throw new TaskStateError(`task ${id} has no pending deletion`);
970
+ // Preserve committed deliveries outside the record before deletion. A crash
971
+ // before unlink may replay two copies; both carry identical service dedupe IDs.
972
+ const removed = { eventId: `task:${id}:deleted`, taskId: id,
973
+ title: `Task deleted: ${stored.title}`.slice(0, 160), body: `Task ${id}: deleting → deleted.`,
974
+ target: { kind: 'task-deleted', taskId: id } };
975
+ replaceFileAtomically(join(retiredNotificationsDir(), `${id}.json`), JSON.stringify({ notification_events: [...(stored.notification_events ?? []), removed] }) + '\n');
768
976
  try {
769
977
  unlinkSync(taskPath(id));
770
978
  }
@@ -776,3 +984,103 @@ export function unlinkDeletedTask(id) {
776
984
  return true;
777
985
  });
778
986
  }
987
+ /** Called only after current temp and exact identity absence have been checked. */
988
+ export function settleAbsentTaskDeletionMember(id, name) {
989
+ withTaskLock(id, () => {
990
+ const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
991
+ if (stored.deletion?.status !== 'pending')
992
+ throw new TaskStateError('Task is not deleting');
993
+ const member = stored.deletion.members.find(value => value.name === name);
994
+ if (!member)
995
+ throw new TaskStateError('Unknown deletion member');
996
+ member.phase = 'identity_absent';
997
+ member.launch_id ??= DELETION_MEMBER_ABSENT_VERIFIED;
998
+ member.updated_at = new Date().toISOString();
999
+ writeTask(stored);
1000
+ });
1001
+ }
1002
+ /** Room-only erasure keeps the parent Task but removes its dead room/member links. */
1003
+ export function detachDeletedRoom(taskId, roomId) {
1004
+ withTaskLock(taskId, () => {
1005
+ if (!existsSync(taskPath(taskId)))
1006
+ return;
1007
+ const stored = JSON.parse(readFileSync(taskPath(taskId), 'utf8'));
1008
+ if (stored.room_id !== roomId || stored.deletion?.status === 'pending')
1009
+ return;
1010
+ delete stored.room_id;
1011
+ delete stored.room_identity_cid;
1012
+ stored.member_roles = [];
1013
+ writeTask(stored);
1014
+ });
1015
+ }
1016
+ /** A completed room may consume its archives while other task members still retire. */
1017
+ export function beginTaskArchiveCleanup(id) {
1018
+ withTaskLock(id, () => {
1019
+ const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
1020
+ if (stored.deletion?.status !== 'pending')
1021
+ throw new TaskStateError('Task is not deleting');
1022
+ stored.deletion.workspace_cleanup_started_at ??= new Date().toISOString();
1023
+ writeTask(stored);
1024
+ });
1025
+ }
1026
+ /** Metadata-only edit; compare under the task lock to prevent lost updates. */
1027
+ /**
1028
+ * Choose, change or clear a Backlog task's Room Layout before it starts.
1029
+ * `expected` is the layout name the caller saw (null = none); a different
1030
+ * current choice is a stale edit. Choosing a layout drops the sealed room
1031
+ * template plan; the caller releases the returned launch snapshot hash.
1032
+ */
1033
+ export function updateTaskLayout(id, expected, next) {
1034
+ return withTaskLock(id, () => {
1035
+ const task = readTask(id);
1036
+ assertNoPendingDeletion(task);
1037
+ assertNoPendingTerminalIntent(task);
1038
+ if (task.state !== 'backlog' || task.room_id)
1039
+ throw new TaskStateError(`task ${id} has started; its room layout is fixed (state '${task.state}')`);
1040
+ const current = task.layout?.name ?? null;
1041
+ // A replay of the same choice is idempotent (and refreshes the pinned source hash).
1042
+ if (current !== expected && current !== (next?.name ?? null))
1043
+ throw new TaskStateError(`task ${id} room layout changed to '${current ?? 'one room'}'; reload before changing it`);
1044
+ const releasedSnapshot = next ? task.execution_plan?.snapshot.launch_snapshot_hash : undefined;
1045
+ if (next) {
1046
+ task.layout = { name: next.name, run_id: `task-${task.task_id}`, definition_hash: next.definition_hash };
1047
+ delete task.execution_plan;
1048
+ delete task.template;
1049
+ delete task.no_room;
1050
+ }
1051
+ else
1052
+ delete task.layout;
1053
+ task.room_plan_changed_at = new Date().toISOString();
1054
+ writeTask(task);
1055
+ return { task: readTask(id), ...(releasedSnapshot ? { releasedSnapshot } : {}) };
1056
+ });
1057
+ }
1058
+ /**
1059
+ * Progress and To review edits: one read-modify-write under the task lock, so
1060
+ * concurrent writers never lose each other's changes. Lifecycle state is untouched.
1061
+ */
1062
+ export function updateTaskContribution(id, change) {
1063
+ assertCanonicalTaskId(id);
1064
+ return withTaskLock(id, () => {
1065
+ const task = readTask(id);
1066
+ assertNoPendingDeletion(task);
1067
+ assertNoPendingTerminalIntent(task);
1068
+ const { value, changed } = change(task);
1069
+ if (changed)
1070
+ writeTask(task);
1071
+ return value;
1072
+ });
1073
+ }
1074
+ export function updateTaskBrief(id, brief, expectedBrief) {
1075
+ assertCanonicalTaskId(id);
1076
+ return withTaskLock(id, () => {
1077
+ const task = readTask(id);
1078
+ assertNoPendingDeletion(task);
1079
+ assertNoPendingTerminalIntent(task);
1080
+ if ((task.brief ?? '') !== expectedBrief && task.brief !== brief)
1081
+ throw new TaskStateError('Description changed elsewhere. Reload the task before saving.');
1082
+ task.brief = brief;
1083
+ writeTask(task);
1084
+ return task;
1085
+ });
1086
+ }