@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,455 @@
1
+ use super::*;
2
+ use super::launch_spawn::{
3
+ quick_start_team_dir, seed_healthy_coordinator, DELEG_ROLE_ALPHA, DELEG_ROLE_BRAVO,
4
+ QS_VALID_ROLE,
5
+ };
6
+ use crate::transport::test_support::OfflineTransport;
7
+
8
+ /// A no-owner workspace (= self-contained team dir) with a compiled 2-agent spec (alpha, bravo) + state
9
+ /// listing both at `status`. ensure_owner_allowed passes (no team_owner); load_spec finds alpha/bravo.
10
+ pub(super) fn lanea_team_ws(status: &str) -> PathBuf {
11
+ let ws = temp_ws().join("laneateam");
12
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
13
+ std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
14
+ std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
15
+ std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
16
+ let spec = crate::compiler::compile_team(&ws).expect("compile lane-A team");
17
+ // Re-point routing/tasks at the STAYING agent `bravo` so removing `alpha` validates cleanly (golden
18
+ // remove_agent removes from agents+startup_order then validate_spec raises on dangling refs — a routed
19
+ // agent is not removable in golden; alpha here stands in for an unrouted/dynamic worker). See lanea_ws_agents.
20
+ let yaml = crate::model::yaml::dumps(&spec)
21
+ .replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
22
+ .replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
23
+ .replace("assignee: \"alpha\"", "assignee: \"bravo\"");
24
+ assert!(!yaml.contains("default_assignee: \"alpha\""), "fixture unroute: default_assignee still alpha");
25
+ assert!(!yaml.contains("assign_to: \"alpha\""), "fixture unroute: a routing rule still assign_to alpha");
26
+ assert!(!yaml.contains("assignee: \"alpha\""), "fixture unroute: task still assignee alpha");
27
+ std::fs::write(ws.join("team.spec.yaml"), yaml).unwrap();
28
+ crate::state::persist::save_runtime_state(
29
+ &ws,
30
+ &json!({
31
+ "session_name": "team-laneateam",
32
+ "agents": {
33
+ "alpha": {"status": status, "provider": "codex", "window": "alpha"},
34
+ "bravo": {"status": status, "provider": "codex", "window": "bravo"}
35
+ }
36
+ }),
37
+ )
38
+ .unwrap();
39
+ ws
40
+ }
41
+
42
+ // remove_agent [P0] — from_spec + force on a NON-running agent atomically removes it from state.agents
43
+ // (golden agents.py: pop agents[agent_id] + save). Pure fs/state (non-running -> no stop/tmux). Today the
44
+ // stub returns OwnerRefused and removes nothing -> RED.
45
+ #[test]
46
+ fn lanea_remove_agent_from_spec_force_removes_from_state() {
47
+ let ws = lanea_team_ws("stopped"); // non-running -> the running+force stop branch is skipped (no tmux)
48
+ let _ = remove_agent(&ws, &aid("alpha"), true, true, None); // from_spec=true, force=true
49
+ let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
50
+ let agents = state.get("agents").and_then(serde_json::Value::as_object);
51
+ assert!(
52
+ agents.is_some_and(|a| !a.contains_key("alpha")),
53
+ "remove_agent(from_spec, force) must atomically remove 'alpha' from state.agents (golden agents.py); \
54
+ the stub returns OwnerRefused and removes nothing; state.agents={agents:?}"
55
+ );
56
+ }
57
+
58
+ // remove_agent [P1] — a RUNNING agent removed without --force is RefusedForceRequired (golden
59
+ // agents.py:56; _is_running is status-based for status in {running,busy} -> no tmux). Today the stub
60
+ // returns OwnerRefused (a different, wrong refusal) -> RED.
61
+ #[test]
62
+ fn lanea_remove_agent_running_without_force_is_refused_force_required() {
63
+ let ws = lanea_team_ws("running"); // alpha is running (status-based _is_running -> true, no tmux)
64
+ match remove_agent(&ws, &aid("alpha"), true, false, None) {
65
+ Ok(RemoveAgentOutcome::RefusedForceRequired { agent_id }) => assert_eq!(agent_id, aid("alpha")),
66
+ other => panic!(
67
+ "a RUNNING agent removed without --force must be RefusedForceRequired (golden agents.py:56); \
68
+ the stub returns OwnerRefused; got {other:?}"
69
+ ),
70
+ }
71
+ }
72
+
73
+ // stop_agent [P0] — an UNKNOWN agent id (past the owner gate) raises "unknown worker agent id: <id>"
74
+ // (golden operations.py:73), NOT OwnerRefused. The owner gate passes on a no-owner ws, then _find_agent
75
+ // fails. The check precedes any tmux. Today the stub returns OwnerRefused (never loads the spec) -> RED.
76
+ #[test]
77
+ fn lanea_stop_agent_unknown_agent_is_unknown_worker_not_owner_refused() {
78
+ let ws = lanea_team_ws("stopped");
79
+ let text = format!("{:?}", stop_agent(&ws, &aid("ghost"), None));
80
+ assert!(
81
+ text.contains("unknown worker"),
82
+ "stop_agent past the owner gate must raise 'unknown worker agent id: ghost' for an unknown agent \
83
+ (golden operations.py:73), not OwnerRefused; got {text}"
84
+ );
85
+ }
86
+
87
+ // fork_agent [P0] — precedence: an UNKNOWN source is rejected as "unknown worker agent id" BEFORE the
88
+ // source-session-id check (golden operations.py:284-25). Today the stub always returns "source session_id
89
+ // is missing" (never loads the spec) -> RED.
90
+ #[test]
91
+ fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
92
+ let ws = lanea_team_ws("stopped");
93
+ let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), false, None));
94
+ assert!(
95
+ text.contains("unknown worker"),
96
+ "fork_agent must reject an UNKNOWN source as 'unknown worker agent id: ghost' BEFORE the session-id \
97
+ check (golden precedence); the stub returns 'source session_id is missing'; got {text}"
98
+ );
99
+ }
100
+
101
+ // fork_agent [P0] — precedence: a DUPLICATE fork-target id is rejected as "agent id already exists" BEFORE
102
+ // the source-session-id check (golden operations.py:284-19, the FIRST guard). Today the stub returns
103
+ // "source session_id is missing" -> RED.
104
+ #[test]
105
+ fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
106
+ let ws = lanea_team_ws("stopped");
107
+ // target 'bravo' already exists in the spec -> duplicate; source 'alpha' exists (its session_id is irrelevant).
108
+ let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), false, None));
109
+ assert!(
110
+ text.contains("already exists"),
111
+ "fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
112
+ check (golden precedence, the first guard); the stub returns 'source session_id is missing'; got {text}"
113
+ );
114
+ }
115
+
116
+ // SEAM-GATED real-machine boundary (note to porter): stop_agent / reset_agent / fork_agent must drive a
117
+ // transport (kill_window / re-spawn / native-fork spawn). The porter adds stop_agent_with_transport /
118
+ // reset_agent_with_transport / fork_agent_with_transport(.., &dyn Transport) (mirror restart_with_transport)
119
+ // so kill_window / spawn are assertable in-process via a recording transport. Until then the PUBLIC fns
120
+ // hit real tmux -> #[ignore]. This documents the stop_agent kill_window + mark-stopped observable.
121
+ #[test]
122
+ #[ignore = "real-machine: stop_agent kills the real tmux window. PORTER SEAM: stop_agent_with_transport(.., \
123
+ &dyn Transport) so kill_window(session:window) + agents[a].status='stopped' is assertable in-process."]
124
+ fn lanea_stop_agent_kills_window_and_marks_stopped() {
125
+ let ws = lanea_team_ws("running");
126
+ let _ = stop_agent(&ws, &aid("alpha"), None); // real machine: tmux kill-window team-laneateam:alpha
127
+ let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
128
+ let status = state.pointer("/agents/alpha/status").and_then(serde_json::Value::as_str);
129
+ assert_eq!(status, Some("stopped"), "stop_agent must mark agents[alpha].status='stopped' after killing the window");
130
+ }
131
+
132
+ // ═════════════════════════════════════════════════════════════════════════
133
+ // rt-host-a P1 — `add-agent w2` recompiles + spawns w2 but does NOT JOIN it to the LIVE team: (a) the
134
+ // running roster (state.agents at team_workspace) stays ['w1'] — add_agent upserts w2 into the team-DIR
135
+ // workspace, not the running workspace; (b) w2 is spawned detached / not spawn_into the existing team
136
+ // session. Coordinator can't deliver to w2 -> `send w2` never round-trips. Golden lifecycle/operations.py:
137
+ // add_agent -> start_agent(allow_fresh) spawns INTO the team session + the agent is in runtime state.
138
+ // ═════════════════════════════════════════════════════════════════════════
139
+
140
+ // RED — add_agent_with_transport over a seeded RUNNING team must JOIN w2: (1) the RUNNING roster
141
+ // (load_runtime_state(team_workspace)) gains "w2", AND (2) the transport records a spawn_INTO (not
142
+ // spawn_first) so w2 joins the existing team session. Today the roster stays ['w1'] (w2 written to the
143
+ // wrong workspace) and start_agent_with_transport is a stub (zero spawns) -> RED. OS-safe: recording
144
+ // transport (no real tmux) + seeded-healthy-coordinator (start_coordinator AlreadyRunning).
145
+ #[test]
146
+ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
147
+ let team_dir = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir (agents/implementer.md)
148
+ let workspace = team_dir.parent().expect("team_workspace(team_dir) = parent"); // the RUNNING team's workspace
149
+ // a RUNNING team already in the session (roster = ['w1'], session_name set).
150
+ crate::state::persist::save_runtime_state(
151
+ workspace,
152
+ &json!({
153
+ "session_name": "team-implteam",
154
+ "agents": {"w1": {"status": "running", "provider": "codex", "window": "w1"}}
155
+ }),
156
+ )
157
+ .unwrap();
158
+ seed_healthy_coordinator(workspace); // start_coordinator -> AlreadyRunning (no real daemon)
159
+ // the new agent's role file, OUTSIDE agents/ so it's not a duplicate of an existing agent.
160
+ let role_file = team_dir.join("w2-role.md");
161
+ std::fs::write(
162
+ &role_file,
163
+ "---\nname: w2\nrole: Worker Two\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker two.\n",
164
+ )
165
+ .unwrap();
166
+ let transport = OfflineTransport::new().with_session_present(true);
167
+
168
+ let _ = add_agent_with_transport(team_dir.as_path(), &aid("w2"), &role_file, false, None, &transport);
169
+
170
+ // (1) [load-bearing] the RUNNING roster (team_workspace) must now contain w2.
171
+ let state = crate::state::persist::load_runtime_state(workspace).expect("load running state");
172
+ assert!(
173
+ state.pointer("/agents/w2").is_some(),
174
+ "add-agent must JOIN w2 to the RUNNING roster (state.agents at team_workspace); today w2 is upserted \
175
+ into the team-DIR workspace, not the running workspace, so the roster stays ['w1'] and the \
176
+ coordinator can't deliver to w2; running agents={:?}",
177
+ state.get("agents")
178
+ );
179
+ // (2) w2 must spawn_INTO the existing team session (not a detached/new session via spawn_first).
180
+ let recorded = transport.spawn_records();
181
+ assert!(
182
+ recorded.iter().any(|(kind, _)| kind == "spawn_into"),
183
+ "add-agent must spawn w2 INTO the existing team session (spawn_into); today start_agent_with_transport \
184
+ is a stub -> ZERO spawns recorded; got {recorded:?}"
185
+ );
186
+ assert!(
187
+ !recorded.iter().any(|(kind, _)| kind == "spawn_first"),
188
+ "w2 must NOT create a NEW session (spawn_first) — that's the detached-spawn bug; got {recorded:?}"
189
+ );
190
+ }
191
+
192
+ // REAL-MACHINE e2e boundary (rt-host-a verifies): after `add-agent w2`, the coordinator delivers
193
+ // `send w2` and w2 reports a result (full round-trip). #[ignore] — needs a live tmux session + worker.
194
+ #[test]
195
+ #[ignore = "real-machine: add-agent then send w2 round-trips"]
196
+ fn add_agent_then_send_w2_round_trips() {
197
+ // The framework asserts: add-agent w2 -> w2 in the live session + roster -> `send w2 <msg>` ->
198
+ // coordinator delivers -> w2 emits a result_envelope (results row). Not unit-testable in-process.
199
+ }
200
+
201
+ #[derive(Clone)]
202
+ struct SocketRecordingRunner {
203
+ recorded: std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>,
204
+ }
205
+
206
+ impl crate::tmux_backend::CommandRunner for SocketRecordingRunner {
207
+ fn run(&self, argv: &[String]) -> Result<crate::tmux_backend::CommandOutput, std::io::Error> {
208
+ self.recorded.lock().unwrap().push(argv.to_vec());
209
+ Ok(crate::tmux_backend::CommandOutput {
210
+ success: true,
211
+ code: Some(0),
212
+ stdout: String::new(),
213
+ stderr: String::new(),
214
+ })
215
+ }
216
+ }
217
+
218
+ fn socket_for_workspace(workspace: &std::path::Path) -> String {
219
+ use crate::transport::Transport as _;
220
+ let recorded = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
221
+ let runner = SocketRecordingRunner { recorded: std::sync::Arc::clone(&recorded) };
222
+ let backend = crate::tmux_backend::TmuxBackend::with_runner_for_workspace(
223
+ Box::new(runner),
224
+ workspace,
225
+ );
226
+ backend.has_session(&sess("team-implteam")).unwrap();
227
+ let socket = recorded.lock().unwrap()[0][2].clone();
228
+ socket
229
+ }
230
+
231
+ // BUG A / CP-1: public lifecycle handlers such as stop_agent receive either the run workspace or
232
+ // the team dir, but the daemon is bound to the run workspace socket. A team-dir raw for_workspace
233
+ // derives a different -L socket and makes stop_agent silently see no windows.
234
+ #[test]
235
+ fn bug_a_team_dir_lifecycle_socket_matches_daemon_run_workspace_socket() {
236
+ let run_ws = temp_ws();
237
+ std::fs::create_dir_all(crate::model::paths::runtime_dir(&run_ws)).unwrap();
238
+ let team_dir = run_ws.join("agents");
239
+ std::fs::create_dir_all(&team_dir).unwrap();
240
+
241
+ let daemon_socket = socket_for_workspace(&run_ws);
242
+ let lifecycle_ws = crate::lifecycle::restart::lifecycle_run_workspace(&team_dir).unwrap();
243
+ let stop_socket = socket_for_workspace(&lifecycle_ws);
244
+ assert_eq!(
245
+ stop_socket, daemon_socket,
246
+ "team-dir lifecycle ops must derive the same -L socket as the daemon run workspace"
247
+ );
248
+ assert_ne!(
249
+ socket_for_workspace(&team_dir),
250
+ daemon_socket,
251
+ "regression guard: raw team_dir for_workspace is the buggy socket"
252
+ );
253
+ }
254
+
255
+ #[test]
256
+ #[ignore = "real-machine: stop_agent with a team-dir input kills an existing worker window on the daemon -L socket"]
257
+ fn bug_a_stop_agent_team_dir_input_kills_existing_window_real_machine() {
258
+ let team_dir = quick_start_team_dir(QS_VALID_ROLE);
259
+ let report = stop_agent(&team_dir, &aid("w1"), None).expect("real stop-agent");
260
+ assert!(report.stopped, "existing worker window must be killed, not silently reported absent");
261
+ }
262
+
263
+ // ═════════════════════════════════════════════════════════════════════════
264
+ // WAVE-2 · LANE A v2 — DEEPENED byte-parity REDs (stop / reset / remove / fork).
265
+ //
266
+ // The shallow 5 lanea_ tests pass, but the port is NOT byte-parity. /tmp/lanea_blockers.json caught
267
+ // 15 CONFIRMED blockers + 2 warns. These lock the GOLDEN observable for each.
268
+ // Golden (truth source): lifecycle/operations.py (stop:62 / reset:102 / fork:284),
269
+ // lifecycle/agents.py (remove:22 + _RemoveRollback + _is_running + _find_worker),
270
+ // runtime.py:1023 _tmux_window_exists, display/close.py (close_ghostty_workspace_slot:51).
271
+ //
272
+ // Transport-driven via LaneTransport (records kill_window + spawns; list_windows/list_targets answer
273
+ // from a configurable window set = golden's _tmux_window_exists primitive). OS-safe (no real tmux;
274
+ // seeded-healthy coordinator where start_coordinator is reached). Rollback-internal bits that need a
275
+ // production failure-injection seam (agent_health re-upsert; fork post-spawn arms) are #[ignore]
276
+
277
+ // ═══════════════════════════════════════════════════════════════════════════
278
+ // Wave3 EVENT PAYLOAD BYTE-LOCK — lifecycle verbs must write golden's events.jsonl payloads.
279
+ // events.jsonl = json.dumps({ts, event, **fields}, sort_keys=True) (event_log.rs:4 / golden events.py:35)
280
+ // → byte form has ALPHABETICALLY-sorted keys (NOT insertion order; that is state.json's rule). So the
281
+ // byte-lock = event name + field KEY SET + field VALUES (ts is a live timestamp → tolerated; order is
282
+ // sort_keys-deterministic given keys+values). Rust restart/remove.rs + restart/agent.rs write ZERO
283
+ // lifecycle events (crate-wide grep: the names exist in types.rs but are never emitted) -> RED.
284
+ // ═══════════════════════════════════════════════════════════════════════════
285
+
286
+ // Read every events.jsonl the verb could write to (run-workspace resolves to either the team dir or its
287
+ // parent; read both so the lock is robust to that resolution).
288
+ fn lifecycle_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
289
+ let mut out = crate::event_log::EventLog::new(ws).tail(0).unwrap_or_default();
290
+ if let Ok(parent) = crate::model::paths::team_workspace(ws) {
291
+ if parent != ws {
292
+ out.extend(crate::event_log::EventLog::new(&parent).tail(0).unwrap_or_default());
293
+ }
294
+ }
295
+ out
296
+ }
297
+
298
+ fn payload_keys(event: &serde_json::Value) -> std::collections::BTreeSet<String> {
299
+ event
300
+ .as_object()
301
+ .map(|o| o.keys().filter(|k| *k != "ts" && *k != "event").cloned().collect())
302
+ .unwrap_or_default()
303
+ }
304
+
305
+ fn find_event<'a>(events: &'a [serde_json::Value], name: &str) -> Option<&'a serde_json::Value> {
306
+ events.iter().find(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
307
+ }
308
+
309
+ fn names(events: &[serde_json::Value]) -> Vec<String> {
310
+ events.iter().filter_map(|e| e.get("event").and_then(|v| v.as_str()).map(ToString::to_string)).collect()
311
+ }
312
+
313
+ // remove-agent — golden agents.py:66-147 writes lifecycle.remove_step_completed (per step) +
314
+ // remove_agent.complete. (stopped agent + from_spec + force: no stop step, pure fs/state, no spawn.)
315
+ #[test]
316
+ fn remove_agent_emits_golden_lifecycle_event_payloads() {
317
+ let ws = lanea_team_ws("stopped");
318
+ let _ = remove_agent(&ws, &aid("alpha"), true, true, None); // from_spec=true, force=true
319
+ let events = lifecycle_events(&ws);
320
+
321
+ // remove_agent.complete (golden agents.py:140-147) — EXACT field key set + load-bearing values.
322
+ let complete = find_event(&events, "remove_agent.complete").unwrap_or_else(|| panic!(
323
+ "remove_agent must write `remove_agent.complete` (golden agents.py:140); Rust restart/remove.rs emits NO \
324
+ events. events seen: {:?}", names(&events)
325
+ ));
326
+ let expected: std::collections::BTreeSet<String> =
327
+ ["agent_id", "from_spec", "force", "stopped", "role_file_removed", "cleared_locations"]
328
+ .iter().map(ToString::to_string).collect();
329
+ assert_eq!(payload_keys(complete), expected,
330
+ "remove_agent.complete payload key set must match golden agents.py:140-147; got {:?}", payload_keys(complete));
331
+ assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
332
+ assert_eq!(complete.get("from_spec").and_then(|v| v.as_bool()), Some(true));
333
+ assert_eq!(complete.get("force").and_then(|v| v.as_bool()), Some(true));
334
+
335
+ // lifecycle.remove_step_completed (golden agents.py:66-72) — fired per step; key set {agent_id, step,
336
+ // resource}; the workspace_state + agent_health steps fire for a stopped from_spec remove.
337
+ let step_events: Vec<&serde_json::Value> = events.iter()
338
+ .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("lifecycle.remove_step_completed")).collect();
339
+ let steps: Vec<&str> = step_events.iter().filter_map(|e| e.get("step").and_then(|v| v.as_str())).collect();
340
+ assert!(steps.contains(&"workspace_state") && steps.contains(&"agent_health"),
341
+ "remove_agent must write lifecycle.remove_step_completed for each step (golden agents.py:86,109); got steps {steps:?}");
342
+ let ws_step = step_events.iter().find(|e| e.get("step").and_then(|v| v.as_str()) == Some("workspace_state"))
343
+ .expect("the workspace_state step event");
344
+ let expected_step: std::collections::BTreeSet<String> =
345
+ ["agent_id", "step", "resource"].iter().map(ToString::to_string).collect();
346
+ assert_eq!(payload_keys(ws_step), expected_step,
347
+ "lifecycle.remove_step_completed (non-stop step) payload must be EXACTLY {{agent_id, step, resource}} (golden agents.py:68-70)");
348
+ assert_eq!(ws_step.get("resource").and_then(|v| v.as_str()), Some("state.json:agents"),
349
+ "workspace_state step resource == golden 'state.json:agents' (agents.py:82)");
350
+ }
351
+
352
+ // reset-agent — golden operations.py:123/132 writes discard.session_tombstone {agent_id,
353
+ // discarded_session_id} + reset_agent.complete {agent_id, stopped, started}. OfflineTransport (no spawn).
354
+ #[test]
355
+ fn reset_agent_emits_golden_lifecycle_event_payloads() {
356
+ let ws = lanea_team_ws("running");
357
+ // give alpha a stored session so discard.session_tombstone.discarded_session_id is meaningful (golden operations.py:118).
358
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
359
+ state["agents"]["alpha"]["session_id"] = json!("S-alpha");
360
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
361
+
362
+ let transport = OfflineTransport::new().with_session_present(true);
363
+ let _ = crate::lifecycle::reset_agent_with_transport(&ws, &aid("alpha"), true, false, None, &transport);
364
+ let events = lifecycle_events(&ws);
365
+
366
+ // discard.session_tombstone (golden operations.py:123) — EXACT key set + the discarded session id.
367
+ let tombstone = find_event(&events, "discard.session_tombstone").unwrap_or_else(|| panic!(
368
+ "reset_agent must write `discard.session_tombstone` (golden operations.py:123); Rust restart/agent.rs emits \
369
+ NO events. events seen: {:?}", names(&events)
370
+ ));
371
+ let expected_tomb: std::collections::BTreeSet<String> =
372
+ ["agent_id", "discarded_session_id"].iter().map(ToString::to_string).collect();
373
+ assert_eq!(payload_keys(tombstone), expected_tomb,
374
+ "discard.session_tombstone payload key set must be EXACTLY {{agent_id, discarded_session_id}} (golden operations.py:123)");
375
+ assert_eq!(tombstone.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
376
+ assert_eq!(tombstone.get("discarded_session_id").and_then(|v| v.as_str()), Some("S-alpha"),
377
+ "discarded_session_id == the agent's stored session_id (golden operations.py:118)");
378
+
379
+ // reset_agent.complete (golden operations.py:132) — EXACT key set {agent_id, stopped, started}.
380
+ let complete = find_event(&events, "reset_agent.complete").unwrap_or_else(|| panic!(
381
+ "reset_agent must write `reset_agent.complete` (golden operations.py:132); events seen: {:?}", names(&events)
382
+ ));
383
+ let expected_complete: std::collections::BTreeSet<String> =
384
+ ["agent_id", "stopped", "started"].iter().map(ToString::to_string).collect();
385
+ assert_eq!(payload_keys(complete), expected_complete,
386
+ "reset_agent.complete payload key set must be EXACTLY {{agent_id, stopped, started}} (golden operations.py:132)");
387
+ assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
388
+ }
389
+
390
+ // stop-agent — golden operations.py:98 writes stop_agent.complete {agent_id, target, stopped}
391
+ // (+ stop_agent.window_stop_failed {agent_id, target, stderr} on kill failure). Rust emits none.
392
+ #[test]
393
+ fn stop_agent_emits_golden_complete_event_payload() {
394
+ let ws = lanea_team_ws("running"); // alpha running (window "alpha")
395
+ let transport = OfflineTransport::new().with_session_present(true);
396
+ let _ = crate::lifecycle::stop_agent_with_transport(&ws, &aid("alpha"), None, &transport);
397
+ let events = lifecycle_events(&ws);
398
+
399
+ let complete = find_event(&events, "stop_agent.complete").unwrap_or_else(|| panic!(
400
+ "stop_agent must write `stop_agent.complete` (golden operations.py:98); Rust restart/agent.rs emits NO \
401
+ events for stop. events seen: {:?}", names(&events)
402
+ ));
403
+ let expected: std::collections::BTreeSet<String> =
404
+ ["agent_id", "target", "stopped"].iter().map(ToString::to_string).collect();
405
+ assert_eq!(payload_keys(complete), expected,
406
+ "stop_agent.complete payload key set must be EXACTLY {{agent_id, target, stopped}} (golden operations.py:98)");
407
+ assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
408
+ }
409
+
410
+ // start-agent — golden start.py:225 writes start_agent.agent_start on the spawn path (9 keys). Rust emits
411
+ // ONLY start_agent.noop (common.rs:315, for the already-running path); the fresh-spawn path emits nothing.
412
+ #[test]
413
+ fn start_agent_emits_golden_agent_start_event_payload() {
414
+ let ws = lanea_team_ws("stopped"); // not running -> fresh spawn path (not noop)
415
+ let transport = OfflineTransport::new();
416
+ let _ = crate::lifecycle::start_agent_with_transport(&ws, &aid("alpha"), true, false, true, None, &transport);
417
+ let events = lifecycle_events(&ws);
418
+
419
+ let agent_start = find_event(&events, "start_agent.agent_start").unwrap_or_else(|| panic!(
420
+ "start_agent (fresh-spawn) must write `start_agent.agent_start` (golden start.py:225); Rust emits only \
421
+ start_agent.noop. events seen: {:?}", names(&events)
422
+ ));
423
+ let expected: std::collections::BTreeSet<String> = [
424
+ "agent_id", "provider", "start_mode", "session_id", "session", "window",
425
+ "tmux_start_mode", "command", "mcp_config",
426
+ ].iter().map(ToString::to_string).collect();
427
+ assert_eq!(payload_keys(agent_start), expected,
428
+ "start_agent.agent_start payload key set must match golden start.py:225-235 (9 keys)");
429
+ assert_eq!(agent_start.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
430
+ assert_eq!(agent_start.get("window").and_then(|v| v.as_str()), Some("alpha"),
431
+ "agent_start.window == agent_id (golden start.py:232)");
432
+ }
433
+
434
+ // restart — golden orchestration.py:507 writes ONE restart.resume_decision per non-paused worker
435
+ // (7 keys, NOTE `worker_id` not `agent_id`). Rust emits no restart events.
436
+ #[test]
437
+ fn restart_emits_golden_resume_decision_event_payload() {
438
+ let ws = lanea_team_ws("running"); // alpha + bravo present -> a resume_decision each
439
+ let transport = OfflineTransport::new().with_session_present(true);
440
+ let _ = crate::lifecycle::restart_with_transport(&ws, true, None, &transport);
441
+ let events = lifecycle_events(&ws);
442
+
443
+ let decisions: Vec<&serde_json::Value> = events.iter()
444
+ .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision")).collect();
445
+ assert!(!decisions.is_empty(), "restart must write one `restart.resume_decision` per non-paused worker \
446
+ (golden orchestration.py:128/507); Rust emits NO restart events. events seen: {:?}", names(&events));
447
+ let expected: std::collections::BTreeSet<String> = [
448
+ "worker_id", "has_first_send_at", "has_session_id", "allow_fresh", "decision", "first_send_at", "session_id",
449
+ ].iter().map(ToString::to_string).collect();
450
+ assert_eq!(payload_keys(decisions[0]), expected,
451
+ "restart.resume_decision payload key set must match golden orchestration.py:507-514 (7 keys, `worker_id`)");
452
+ let worker_ids: Vec<&str> = decisions.iter().filter_map(|e| e.get("worker_id").and_then(|v| v.as_str())).collect();
453
+ assert!(worker_ids.contains(&"alpha") || worker_ids.contains(&"bravo"),
454
+ "restart.resume_decision.worker_id must name a real worker (golden); got {worker_ids:?}");
455
+ }