@team-agent/installer 0.2.10 → 0.3.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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -457
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -86
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1239
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -143
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -602
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,780 @@
1
+ use super::*;
2
+ use super::agent::{resolve_team_scoped_state_or_refuse, start_agent_with_transport, stop_agent_at_paths};
3
+ use super::common::*;
4
+ use super::team_state::write_team_state;
5
+
6
+ /// `remove_agent(workspace, agent_id, from_spec, force, team)`(`lifecycle/agents.py:22`)。
7
+ /// 从 spec/state/team_state/role-file/agent_health 原子摘除;`_RemoveRollback` 字节级快照
8
+ /// 回滚全部。未传 from_spec 确认 / 运行中未传 force → 拒绝。
9
+ pub fn remove_agent(
10
+ workspace: &Path,
11
+ agent_id: &AgentId,
12
+ from_spec: bool,
13
+ force: bool,
14
+ team: Option<&str>,
15
+ ) -> Result<RemoveAgentOutcome, LifecycleError> {
16
+ remove_agent_with_transport(
17
+ workspace,
18
+ agent_id,
19
+ from_spec,
20
+ force,
21
+ team,
22
+ &crate::tmux_backend::TmuxBackend::for_workspace(&lifecycle_run_workspace(workspace)?),
23
+ )
24
+ }
25
+
26
+ pub fn remove_agent_with_transport(
27
+ workspace: &Path,
28
+ agent_id: &AgentId,
29
+ from_spec: bool,
30
+ force: bool,
31
+ team: Option<&str>,
32
+ transport: &dyn crate::transport::Transport,
33
+ ) -> Result<RemoveAgentOutcome, LifecycleError> {
34
+ let paths = lifecycle_paths(workspace, team)?;
35
+ remove_agent_at_paths(
36
+ &paths.run_workspace,
37
+ &paths.spec_workspace,
38
+ agent_id,
39
+ from_spec,
40
+ force,
41
+ team,
42
+ transport,
43
+ )
44
+ }
45
+
46
+ fn remove_agent_at_paths(
47
+ workspace: &Path,
48
+ spec_workspace: &Path,
49
+ agent_id: &AgentId,
50
+ from_spec: bool,
51
+ force: bool,
52
+ team: Option<&str>,
53
+ transport: &dyn crate::transport::Transport,
54
+ ) -> Result<RemoveAgentOutcome, LifecycleError> {
55
+ // golden agents.py:34-41: resolve_team_scoped_state FIRST (surfaces the team_target_ambiguous /
56
+ // team_target_unresolved refusal before the owner gate), THEN the owner gate, THEN load_spec +
57
+ // _find_worker (unknown-worker raise). Mirror the stop/reset wiring so remove is byte-symmetric:
58
+ // the team-scoped projection (not a raw load) drives the dynamic/running/from_spec decisions.
59
+ let state = resolve_team_scoped_state_or_refuse(workspace, team)?;
60
+ crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, Some(agent_id))?;
61
+ let spec = load_team_spec(spec_workspace)?;
62
+ let Some(spec_agent) = find_spec_agent(&spec, agent_id) else {
63
+ return Err(unknown_worker(agent_id));
64
+ };
65
+ let dynamic_agent = is_dynamic_agent(&state, spec_agent, agent_id);
66
+ if !dynamic_agent && !from_spec {
67
+ return Ok(RemoveAgentOutcome::RefusedFromSpecConfirm {
68
+ agent_id: agent_id.clone(),
69
+ });
70
+ }
71
+ if agent_is_running(&state, agent_id, transport) && !force {
72
+ return Ok(RemoveAgentOutcome::RefusedForceRequired {
73
+ agent_id: agent_id.clone(),
74
+ });
75
+ }
76
+ let paths = LifecyclePathRefs {
77
+ run_workspace: workspace,
78
+ spec_workspace,
79
+ };
80
+ let mut rollback = RemoveRollback::capture(paths.run_workspace, paths.spec_workspace, &spec, &state, agent_id)?;
81
+ rollback.restore_running = force && agent_is_running(&state, agent_id, transport);
82
+ let result = remove_agent_inner(
83
+ &paths,
84
+ agent_id,
85
+ &spec,
86
+ state,
87
+ force,
88
+ team,
89
+ transport,
90
+ );
91
+ match result {
92
+ Ok(success) => {
93
+ // golden agents.py:135: _save_team_runtime_snapshot runs OUTSIDE the try/except, and
94
+ // snapshot.py:19-21 returns None (no error) when session_name is falsy. Mirror that here:
95
+ // only snapshot when session_name is present, and never let it roll the committed removal back.
96
+ if success
97
+ .removed_state
98
+ .get("session_name")
99
+ .and_then(|v| v.as_str())
100
+ .is_some_and(|s| !s.is_empty())
101
+ {
102
+ let _ = crate::lifecycle::helpers::save_team_runtime_snapshot(workspace, &success.removed_state)?;
103
+ }
104
+ write_remove_complete_event(
105
+ paths.run_workspace,
106
+ agent_id,
107
+ from_spec,
108
+ force,
109
+ success.stopped,
110
+ success.role_file_removed,
111
+ success.cleared_locations,
112
+ )?;
113
+ Ok(success.outcome)
114
+ }
115
+ Err(error) => {
116
+ // golden agents.py:110-133: restore is best-effort (collects per-artifact errors, restores ALL),
117
+ // and the ORIGINAL operation error is ALWAYS re-raised, annotated with rollback_ok — a
118
+ // restore-step failure only flips rollback_ok, it never replaces the surfaced cause.
119
+ let restore_errors = rollback.restore(paths.run_workspace, paths.spec_workspace, team, transport);
120
+ let rollback_ok = restore_errors.is_empty();
121
+ let rollback_event = RemoveRollbackEvent {
122
+ agent_id,
123
+ workspace: paths.run_workspace,
124
+ from_spec,
125
+ force,
126
+ stopped: rollback.restore_running,
127
+ error: &error,
128
+ rollback_ok,
129
+ restore_errors: &restore_errors,
130
+ };
131
+ let _ = write_remove_rollback_events(rollback_event);
132
+ Err(LifecycleError::StatePersist(format!(
133
+ "remove-agent failed for {agent_id}: {error}; rollback_ok={rollback_ok}"
134
+ )))
135
+ }
136
+ }
137
+ }
138
+
139
+ fn remove_agent_inner(
140
+ paths: &LifecyclePathRefs<'_>,
141
+ agent_id: &AgentId,
142
+ spec: &YamlValue,
143
+ state: serde_json::Value,
144
+ force: bool,
145
+ team: Option<&str>,
146
+ transport: &dyn crate::transport::Transport,
147
+ ) -> Result<RemoveSuccess, LifecycleError> {
148
+ // golden agents.py:75-79: when force-stopping a running worker, RE-RESOLVE the team-scoped state
149
+ // after the stop (stop_agent persisted it); otherwise the originally-resolved projection drives the
150
+ // removal. Either way we operate on the PROJECTION, never a raw load_runtime_state.
151
+ let mut working_state = state;
152
+ let mut stopped = false;
153
+ let mut cleared_locations = Vec::new();
154
+ if force && agent_is_running(&working_state, agent_id, transport) {
155
+ let stop = stop_agent_at_paths(paths.run_workspace, paths.spec_workspace, agent_id, team, transport)?;
156
+ stopped = stop.stopped;
157
+ write_remove_step_event(
158
+ paths.run_workspace,
159
+ agent_id,
160
+ "stop",
161
+ &stop.target,
162
+ Some(stop.stopped),
163
+ )?;
164
+ working_state = resolve_team_scoped_state_or_refuse(paths.run_workspace, team)?;
165
+ }
166
+ let dynamic_role_path = dynamic_role_file_path(paths.run_workspace, &working_state, agent_id);
167
+ let dynamic_role_required = has_recorded_dynamic_role_file(&working_state, agent_id);
168
+ // golden agents.py:81-83: removed_state = deepcopy(state); pop the agent; save_team_scoped_state
169
+ // (team projection) — NOT a raw save, so other teams in a multi-team workspace are preserved.
170
+ let mut removed_state = working_state;
171
+ remove_agent_from_state(&mut removed_state, agent_id)?;
172
+ crate::state::projection::save_team_scoped_state(paths.run_workspace, &removed_state)
173
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
174
+ cleared_locations.push(serde_json::json!("state.json:agents"));
175
+ write_remove_step_event(
176
+ paths.run_workspace,
177
+ agent_id,
178
+ "workspace_state",
179
+ "state.json:agents",
180
+ None,
181
+ )?;
182
+
183
+ let removed_spec = spec_without_agent(spec, agent_id);
184
+ if should_validate_removed_spec(&removed_spec, paths) {
185
+ crate::model::spec::validate_spec(&removed_spec, paths.run_workspace)
186
+ .map_err(|e| LifecycleError::Compile(e.to_string()))?;
187
+ }
188
+ // golden agents.py:96-100,157: state_file = the team_state.md path written from removed_spec/state.
189
+ let team_state_path = write_team_state(paths.spec_workspace, &removed_spec, &removed_state)?;
190
+ cleared_locations.push(serde_json::json!(team_state_path.to_string_lossy().to_string()));
191
+ write_remove_step_event(
192
+ paths.run_workspace,
193
+ agent_id,
194
+ "team_state",
195
+ &team_state_path.to_string_lossy(),
196
+ None,
197
+ )?;
198
+ std::fs::write(paths.spec_workspace.join("team.spec.yaml"), yaml::dumps(&removed_spec))
199
+ .map_err(|e| LifecycleError::StatePersist(format!("write spec: {e}")))?;
200
+ cleared_locations.push(serde_json::json!("team.spec.yaml"));
201
+ write_remove_step_event(
202
+ paths.run_workspace,
203
+ agent_id,
204
+ "spec",
205
+ "team.spec.yaml",
206
+ None,
207
+ )?;
208
+ let role_file_removed = remove_dynamic_role_file(&dynamic_role_path, dynamic_role_required)?;
209
+ if role_file_removed {
210
+ let resource = dynamic_role_path.to_string_lossy().to_string();
211
+ cleared_locations.push(serde_json::json!(resource));
212
+ write_remove_step_event(
213
+ paths.run_workspace,
214
+ agent_id,
215
+ "role_file",
216
+ &dynamic_role_path.to_string_lossy(),
217
+ None,
218
+ )?;
219
+ }
220
+ let agent_health_deleted = delete_agent_health(paths.run_workspace, agent_id)?;
221
+ cleared_locations.push(serde_json::json!("agent_health"));
222
+ write_remove_step_event(
223
+ paths.run_workspace,
224
+ agent_id,
225
+ "agent_health",
226
+ "agent_health",
227
+ None,
228
+ )?;
229
+ Ok(RemoveSuccess {
230
+ outcome: RemoveAgentOutcome::Removed {
231
+ agent_id: agent_id.clone(),
232
+ state_file: team_state_path,
233
+ agent_health_deleted: agent_health_deleted || role_file_removed,
234
+ },
235
+ removed_state,
236
+ stopped,
237
+ role_file_removed,
238
+ cleared_locations,
239
+ })
240
+ }
241
+
242
+ fn should_validate_removed_spec(spec: &YamlValue, paths: &LifecyclePathRefs<'_>) -> bool {
243
+ let agents_empty = spec
244
+ .get("agents")
245
+ .and_then(YamlValue::as_list)
246
+ .is_none_or(|agents| agents.is_empty());
247
+ !(agents_empty && paths.spec_workspace != paths.run_workspace)
248
+ }
249
+
250
+ struct RemoveSuccess {
251
+ outcome: RemoveAgentOutcome,
252
+ removed_state: serde_json::Value,
253
+ stopped: bool,
254
+ role_file_removed: bool,
255
+ cleared_locations: Vec<serde_json::Value>,
256
+ }
257
+
258
+ fn write_remove_step_event(
259
+ workspace: &Path,
260
+ agent_id: &AgentId,
261
+ step: &str,
262
+ resource: &str,
263
+ stopped: Option<bool>,
264
+ ) -> Result<(), LifecycleError> {
265
+ let mut payload = serde_json::Map::new();
266
+ payload.insert("agent_id".to_string(), serde_json::json!(agent_id.as_str()));
267
+ payload.insert("step".to_string(), serde_json::json!(step));
268
+ payload.insert("resource".to_string(), serde_json::json!(resource));
269
+ if let Some(stopped) = stopped {
270
+ payload.insert("stopped".to_string(), serde_json::json!(stopped));
271
+ }
272
+ crate::event_log::EventLog::new(workspace)
273
+ .write(
274
+ crate::lifecycle::types::event_names::REMOVE_STEP_COMPLETED,
275
+ serde_json::Value::Object(payload),
276
+ )
277
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
278
+ Ok(())
279
+ }
280
+
281
+ fn write_remove_complete_event(
282
+ workspace: &Path,
283
+ agent_id: &AgentId,
284
+ from_spec: bool,
285
+ force: bool,
286
+ stopped: bool,
287
+ role_file_removed: bool,
288
+ cleared_locations: Vec<serde_json::Value>,
289
+ ) -> Result<(), LifecycleError> {
290
+ crate::event_log::EventLog::new(workspace)
291
+ .write(
292
+ "remove_agent.complete",
293
+ serde_json::json!({
294
+ "agent_id": agent_id.as_str(),
295
+ "from_spec": from_spec,
296
+ "force": force,
297
+ "stopped": stopped,
298
+ "role_file_removed": role_file_removed,
299
+ "cleared_locations": cleared_locations,
300
+ }),
301
+ )
302
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
303
+ Ok(())
304
+ }
305
+
306
+ struct RemoveRollbackEvent<'a> {
307
+ workspace: &'a Path,
308
+ agent_id: &'a AgentId,
309
+ from_spec: bool,
310
+ force: bool,
311
+ stopped: bool,
312
+ error: &'a LifecycleError,
313
+ rollback_ok: bool,
314
+ restore_errors: &'a [String],
315
+ }
316
+
317
+ fn write_remove_rollback_events(event: RemoveRollbackEvent<'_>) -> Result<(), LifecycleError> {
318
+ let log = crate::event_log::EventLog::new(event.workspace);
319
+ let errors = event
320
+ .restore_errors
321
+ .iter()
322
+ .map(|e| serde_json::json!(e))
323
+ .collect::<Vec<_>>();
324
+ log.write(
325
+ "remove_agent.rollback",
326
+ serde_json::json!({
327
+ "agent_id": event.agent_id.as_str(),
328
+ "from_spec": event.from_spec,
329
+ "force": event.force,
330
+ "stopped": event.stopped,
331
+ "error": event.error.to_string(),
332
+ "rollback_ok": event.rollback_ok,
333
+ "errors": errors,
334
+ }),
335
+ )
336
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
337
+ log.write(
338
+ crate::lifecycle::types::event_names::REMOVE_ROLLED_BACK,
339
+ serde_json::json!({
340
+ "agent_id": event.agent_id.as_str(),
341
+ "step": "rollback",
342
+ "resource": "workspace",
343
+ "rollback_ok": event.rollback_ok,
344
+ "errors": errors,
345
+ }),
346
+ )
347
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
348
+ if !event.restore_errors.is_empty() {
349
+ log.write(
350
+ "remove_agent.rollback_failed",
351
+ serde_json::json!({
352
+ "agent_id": event.agent_id.as_str(),
353
+ "errors": errors,
354
+ }),
355
+ )
356
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
357
+ }
358
+ Ok(())
359
+ }
360
+
361
+ fn remove_agent_from_state(
362
+ state: &mut serde_json::Value,
363
+ agent_id: &AgentId,
364
+ ) -> Result<(), LifecycleError> {
365
+ if let Some(agents) = state.get_mut("agents").and_then(|v| v.as_object_mut()) {
366
+ agents.remove(agent_id.as_str());
367
+ Ok(())
368
+ } else {
369
+ Err(LifecycleError::StatePersist(
370
+ "runtime state agents is not an object".to_string(),
371
+ ))
372
+ }
373
+ }
374
+
375
+ /// Build the persisted spec after removing one worker. Besides deleting the worker and startup entry,
376
+ /// prune routing references that would otherwise point at the removed worker.
377
+ fn spec_without_agent(spec: &YamlValue, agent_id: &AgentId) -> YamlValue {
378
+ let YamlValue::Map(pairs) = spec else {
379
+ return spec.clone();
380
+ };
381
+ let mut out = Vec::new();
382
+ for (key, value) in pairs {
383
+ if key == "agents" {
384
+ let agents = value
385
+ .as_list()
386
+ .map(|items| {
387
+ items
388
+ .iter()
389
+ .filter(|agent| {
390
+ agent
391
+ .get("id")
392
+ .and_then(YamlValue::as_str)
393
+ .map(|id| id != agent_id.as_str())
394
+ .unwrap_or(true)
395
+ })
396
+ .cloned()
397
+ .collect::<Vec<_>>()
398
+ })
399
+ .unwrap_or_default();
400
+ out.push((key.clone(), YamlValue::List(agents)));
401
+ } else if key == "runtime" {
402
+ out.push((key.clone(), runtime_without_startup_agent(value, agent_id)));
403
+ } else if key == "routing" {
404
+ out.push((key.clone(), routing_without_agent(value, agent_id)));
405
+ } else if key == "tasks" {
406
+ out.push((key.clone(), tasks_without_agent_assignee(value, agent_id)));
407
+ } else {
408
+ out.push((key.clone(), value.clone()));
409
+ }
410
+ }
411
+ YamlValue::Map(out)
412
+ }
413
+
414
+ fn runtime_without_startup_agent(runtime: &YamlValue, agent_id: &AgentId) -> YamlValue {
415
+ let YamlValue::Map(pairs) = runtime else {
416
+ return runtime.clone();
417
+ };
418
+ let mut out = Vec::new();
419
+ for (key, value) in pairs {
420
+ if key == "startup_order" {
421
+ let order = value
422
+ .as_list()
423
+ .map(|items| {
424
+ items
425
+ .iter()
426
+ .filter(|item| item.as_str().map(|id| id != agent_id.as_str()).unwrap_or(true))
427
+ .cloned()
428
+ .collect::<Vec<_>>()
429
+ })
430
+ .unwrap_or_default();
431
+ out.push((key.clone(), YamlValue::List(order)));
432
+ } else {
433
+ out.push((key.clone(), value.clone()));
434
+ }
435
+ }
436
+ YamlValue::Map(out)
437
+ }
438
+
439
+ fn routing_without_agent(routing: &YamlValue, agent_id: &AgentId) -> YamlValue {
440
+ let YamlValue::Map(pairs) = routing else {
441
+ return routing.clone();
442
+ };
443
+ let mut out = Vec::new();
444
+ for (key, value) in pairs {
445
+ if key == "default_assignee"
446
+ && value.as_str().is_some_and(|id| id == agent_id.as_str())
447
+ {
448
+ out.push((key.clone(), YamlValue::Str(String::new())));
449
+ } else if key == "rules" {
450
+ let rules = value
451
+ .as_list()
452
+ .map(|items| {
453
+ items
454
+ .iter()
455
+ .filter(|rule| {
456
+ rule
457
+ .get("assign_to")
458
+ .and_then(YamlValue::as_str)
459
+ .map(|id| id != agent_id.as_str())
460
+ .unwrap_or(true)
461
+ })
462
+ .cloned()
463
+ .collect::<Vec<_>>()
464
+ })
465
+ .unwrap_or_default();
466
+ out.push((key.clone(), YamlValue::List(rules)));
467
+ } else {
468
+ out.push((key.clone(), value.clone()));
469
+ }
470
+ }
471
+ YamlValue::Map(out)
472
+ }
473
+
474
+ fn tasks_without_agent_assignee(tasks: &YamlValue, agent_id: &AgentId) -> YamlValue {
475
+ let YamlValue::List(items) = tasks else {
476
+ return tasks.clone();
477
+ };
478
+ YamlValue::List(
479
+ items
480
+ .iter()
481
+ .map(|task| task_without_agent_assignee(task, agent_id))
482
+ .collect(),
483
+ )
484
+ }
485
+
486
+ fn task_without_agent_assignee(task: &YamlValue, agent_id: &AgentId) -> YamlValue {
487
+ let YamlValue::Map(pairs) = task else {
488
+ return task.clone();
489
+ };
490
+ YamlValue::Map(
491
+ pairs
492
+ .iter()
493
+ .map(|(key, value)| {
494
+ if key == "assignee"
495
+ && value
496
+ .as_str()
497
+ .is_some_and(|id| id == agent_id.as_str())
498
+ {
499
+ (key.clone(), YamlValue::Str(String::new()))
500
+ } else {
501
+ (key.clone(), value.clone())
502
+ }
503
+ })
504
+ .collect(),
505
+ )
506
+ }
507
+
508
+ fn remove_dynamic_role_file(
509
+ path: &Path,
510
+ required: bool,
511
+ ) -> Result<bool, LifecycleError> {
512
+ match std::fs::remove_file(path) {
513
+ Ok(()) => Ok(true),
514
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound && required => {
515
+ Err(LifecycleError::StatePersist(format!(
516
+ "dynamic role file missing: {}",
517
+ path.display()
518
+ )))
519
+ }
520
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
521
+ Err(e) => Err(LifecycleError::StatePersist(format!(
522
+ "remove role file {}: {e}",
523
+ path.display()
524
+ ))),
525
+ }
526
+ }
527
+
528
+ fn dynamic_role_file_path(
529
+ workspace: &Path,
530
+ state: &serde_json::Value,
531
+ agent_id: &AgentId,
532
+ ) -> std::path::PathBuf {
533
+ if let Some(raw) = state
534
+ .get("agents")
535
+ .and_then(|v| v.get(agent_id.as_str()))
536
+ .and_then(|v| v.get("dynamic_role_file"))
537
+ .and_then(|v| v.as_str())
538
+ .filter(|s| !s.is_empty())
539
+ {
540
+ let path = std::path::PathBuf::from(raw);
541
+ if path.is_absolute() {
542
+ return path;
543
+ }
544
+ return workspace.join(path);
545
+ }
546
+ workspace
547
+ .join(".team")
548
+ .join("dynamic-role-files")
549
+ .join(format!("{}.md", agent_id.as_str()))
550
+ }
551
+
552
+ fn has_recorded_dynamic_role_file(state: &serde_json::Value, agent_id: &AgentId) -> bool {
553
+ state
554
+ .get("agents")
555
+ .and_then(|v| v.get(agent_id.as_str()))
556
+ .and_then(|v| v.get("dynamic_role_file"))
557
+ .and_then(|v| v.as_str())
558
+ .is_some_and(|s| !s.is_empty())
559
+ }
560
+
561
+ fn delete_agent_health(workspace: &Path, agent_id: &AgentId) -> Result<bool, LifecycleError> {
562
+ let store = crate::message_store::MessageStore::open(workspace)
563
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
564
+ let conn = crate::db::schema::open_db(store.db_path())
565
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
566
+ let changed = conn
567
+ .execute("delete from agent_health where agent_id = ?1", [agent_id.as_str()])
568
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
569
+ Ok(changed > 0)
570
+ }
571
+
572
+ /// golden agents.py:185 `copy.deepcopy(store.agent_health().get(agent_id))` — read the row BEFORE delete
573
+ /// so the rollback can re-upsert it. Returns the captured health columns, or None if absent.
574
+ fn select_agent_health(
575
+ workspace: &Path,
576
+ agent_id: &AgentId,
577
+ ) -> Result<Option<CapturedHealth>, LifecycleError> {
578
+ let store = crate::message_store::MessageStore::open(workspace)
579
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
580
+ let conn = crate::db::schema::open_db(store.db_path())
581
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
582
+ let row = conn
583
+ .query_row(
584
+ "select status, last_output_at, context_usage_pct, current_task_id \
585
+ from agent_health where agent_id = ?1",
586
+ [agent_id.as_str()],
587
+ |r| {
588
+ Ok(CapturedHealth {
589
+ status: r.get::<_, Option<String>>(0)?,
590
+ last_output_at: r.get::<_, Option<String>>(1)?,
591
+ context_usage_pct: r.get::<_, Option<i64>>(2)?,
592
+ current_task_id: r.get::<_, Option<String>>(3)?,
593
+ })
594
+ },
595
+ )
596
+ .ok();
597
+ Ok(row)
598
+ }
599
+
600
+ /// golden agents.py:268-278 `_restore_agent_health`: re-upsert the captured row (status||"IDLE"), or
601
+ /// delete the row when there was nothing to restore.
602
+ fn restore_agent_health(
603
+ workspace: &Path,
604
+ agent_id: &AgentId,
605
+ row: &Option<CapturedHealth>,
606
+ ) -> Result<(), LifecycleError> {
607
+ let Some(row) = row else {
608
+ delete_agent_health(workspace, agent_id)?;
609
+ return Ok(());
610
+ };
611
+ let store = crate::message_store::MessageStore::open(workspace)
612
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
613
+ let conn = crate::db::schema::open_db(store.db_path())
614
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
615
+ let status = row.status.clone().unwrap_or_else(|| "IDLE".to_string());
616
+ let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.6f+00:00").to_string();
617
+ // The restore always follows a delete of this row, so a plain insert re-materializes the captured
618
+ // health (golden _restore_agent_health re-upserts status||"IDLE" + the captured columns).
619
+ conn.execute(
620
+ "insert into agent_health (owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at) \
621
+ values (null, ?1, ?2, ?3, ?4, ?5, ?6)",
622
+ rusqlite::params![
623
+ agent_id.as_str(),
624
+ status,
625
+ row.last_output_at,
626
+ row.context_usage_pct,
627
+ row.current_task_id,
628
+ now,
629
+ ],
630
+ )
631
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
632
+ Ok(())
633
+ }
634
+
635
+ #[derive(Clone)]
636
+ struct CapturedHealth {
637
+ status: Option<String>,
638
+ last_output_at: Option<String>,
639
+ context_usage_pct: Option<i64>,
640
+ current_task_id: Option<String>,
641
+ }
642
+
643
+ struct RemoveRollback {
644
+ agent_id: AgentId,
645
+ spec_text: Option<String>,
646
+ state: serde_json::Value,
647
+ team_state_text: Option<String>,
648
+ team_state_path: std::path::PathBuf,
649
+ dynamic_role_bytes: Option<Vec<u8>>,
650
+ dynamic_role_path: std::path::PathBuf,
651
+ /// golden agents.py:185: the agent_health row captured BEFORE delete, re-upserted on rollback.
652
+ health: Option<CapturedHealth>,
653
+ restore_running: bool,
654
+ }
655
+
656
+ impl RemoveRollback {
657
+ fn capture(
658
+ workspace: &Path,
659
+ spec_workspace: &Path,
660
+ spec: &YamlValue,
661
+ state: &serde_json::Value,
662
+ agent_id: &AgentId,
663
+ ) -> Result<Self, LifecycleError> {
664
+ let spec_path = spec_workspace.join("team.spec.yaml");
665
+ let spec_text = match std::fs::read_to_string(&spec_path) {
666
+ Ok(text) => Some(text),
667
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
668
+ Err(e) => return Err(LifecycleError::StatePersist(format!("read spec: {e}"))),
669
+ };
670
+ let team_state_path = spec_workspace.join(
671
+ spec.get("context")
672
+ .and_then(|v| v.get("state_file"))
673
+ .and_then(YamlValue::as_str)
674
+ .unwrap_or("team_state.md"),
675
+ );
676
+ let team_state_text = match std::fs::read_to_string(&team_state_path) {
677
+ Ok(text) => Some(text),
678
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
679
+ Err(e) => return Err(LifecycleError::StatePersist(format!("read team_state: {e}"))),
680
+ };
681
+ let dynamic_role_path = dynamic_role_file_path(workspace, state, agent_id);
682
+ let dynamic_role_bytes = match std::fs::read(&dynamic_role_path) {
683
+ Ok(bytes) => Some(bytes),
684
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
685
+ Err(e) => return Err(LifecycleError::StatePersist(format!("read role file: {e}"))),
686
+ };
687
+ let health = select_agent_health(workspace, agent_id)?;
688
+ Ok(Self {
689
+ agent_id: agent_id.clone(),
690
+ spec_text,
691
+ state: state.clone(),
692
+ team_state_text,
693
+ team_state_path,
694
+ dynamic_role_bytes,
695
+ dynamic_role_path,
696
+ health,
697
+ restore_running: false,
698
+ })
699
+ }
700
+
701
+ /// golden agents.py:189-227 `_RemoveRollback.restore`: BEST-EFFORT — wrap EACH artifact restore
702
+ /// (spec → workspace_state → team_state → role_file → agent_health) in its own try/except, append
703
+ /// per-artifact failures to `errors`, and NEVER short-circuit on the first failure. The worker is
704
+ /// only re-started when restore_running AND no errors. Returns the collected error strings (empty
705
+ /// == ok); the caller re-raises the ORIGINAL operation error annotated with rollback_ok.
706
+ fn restore(
707
+ &self,
708
+ workspace: &Path,
709
+ spec_workspace: &Path,
710
+ team: Option<&str>,
711
+ transport: &dyn crate::transport::Transport,
712
+ ) -> Vec<String> {
713
+ let mut errors: Vec<String> = Vec::new();
714
+
715
+ // spec
716
+ let spec_path = spec_workspace.join("team.spec.yaml");
717
+ if let Some(text) = &self.spec_text {
718
+ if let Err(e) = std::fs::write(&spec_path, text) {
719
+ errors.push(format!("spec:{e}"));
720
+ }
721
+ }
722
+ // workspace_state
723
+ if let Err(e) = crate::state::persist::save_runtime_state(workspace, &self.state) {
724
+ errors.push(format!("workspace_state:{e}"));
725
+ }
726
+ // team_state
727
+ let team_state_result = match &self.team_state_text {
728
+ Some(text) => {
729
+ if let Some(parent) = self.team_state_path.parent() {
730
+ let _ = std::fs::create_dir_all(parent);
731
+ }
732
+ std::fs::write(&self.team_state_path, text)
733
+ }
734
+ None => match std::fs::remove_file(&self.team_state_path) {
735
+ Ok(()) => Ok(()),
736
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
737
+ Err(e) => Err(e),
738
+ },
739
+ };
740
+ if let Err(e) = team_state_result {
741
+ errors.push(format!("team_state:{e}"));
742
+ }
743
+ // role_file
744
+ let role_file_result = match &self.dynamic_role_bytes {
745
+ Some(bytes) => {
746
+ if let Some(parent) = self.dynamic_role_path.parent() {
747
+ let _ = std::fs::create_dir_all(parent);
748
+ }
749
+ std::fs::write(&self.dynamic_role_path, bytes)
750
+ }
751
+ None => match std::fs::remove_file(&self.dynamic_role_path) {
752
+ Ok(()) => Ok(()),
753
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
754
+ Err(e) => Err(e),
755
+ },
756
+ };
757
+ if let Err(e) = role_file_result {
758
+ errors.push(format!("role_file:{e}"));
759
+ }
760
+ // agent_health
761
+ if let Err(e) = restore_agent_health(workspace, &self.agent_id, &self.health) {
762
+ errors.push(format!("agent_health:{e}"));
763
+ }
764
+
765
+ if self.restore_running && errors.is_empty() {
766
+ if let Err(e) = start_agent_with_transport(
767
+ workspace,
768
+ &self.agent_id,
769
+ true,
770
+ false,
771
+ true,
772
+ team,
773
+ transport,
774
+ ) {
775
+ errors.push(format!("worker_restore:{e}"));
776
+ }
777
+ }
778
+ errors
779
+ }
780
+ }