@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,583 @@
1
+ use super::*;
2
+ use super::launch_spawn::{seed_healthy_coordinator, DELEG_ROLE_ALPHA, DELEG_ROLE_BRAVO};
3
+
4
+ // ═════════════════════════════════════════════════════════════════════════
5
+
6
+ const DELEG_ROLE_ALPHA_COMPAT: &str = "---\nname: alpha\nrole: Alpha Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: compatible_api\nprofile: alpha-compat\ntools:\n - mcp_team\n---\n\nAlpha.\n";
7
+
8
+ type LaneKills = std::sync::Arc<std::sync::Mutex<Vec<String>>>;
9
+ pub(super) type LaneSpawns = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<String>)>>>;
10
+
11
+ /// Recording transport for Lane-A v2: `list_windows`/`list_targets` answer from a configurable window
12
+ /// set (golden's `_tmux_window_exists` primitive = `tmux list-windows`); `kill_window` + spawn_first/into
13
+ /// are RECORDED. Every other method returns a benign Ok (never panics) so stop/reset/remove/fork run
14
+ /// end-to-end in-process.
15
+ pub(super) struct LaneTransport {
16
+ session: String,
17
+ windows: Vec<String>,
18
+ killed: LaneKills,
19
+ spawns: LaneSpawns,
20
+ }
21
+ impl LaneTransport {
22
+ pub(super) fn new(session: &str, windows: &[&str]) -> Self {
23
+ Self {
24
+ session: session.to_string(),
25
+ windows: windows.iter().map(|w| (*w).to_string()).collect(),
26
+ killed: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
27
+ spawns: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
28
+ }
29
+ }
30
+ fn killed(&self) -> Vec<String> {
31
+ self.killed.lock().unwrap().clone()
32
+ }
33
+ fn spawns(&self) -> Vec<(String, Vec<String>)> {
34
+ self.spawns.lock().unwrap().clone()
35
+ }
36
+ }
37
+ impl crate::transport::Transport for LaneTransport {
38
+ fn kind(&self) -> crate::transport::BackendKind {
39
+ crate::transport::BackendKind::Tmux
40
+ }
41
+ fn spawn_first(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
42
+ self.spawns.lock().unwrap().push(("spawn_first".to_string(), argv.to_vec()));
43
+ Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid: None })
44
+ }
45
+ fn spawn_into(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
46
+ self.spawns.lock().unwrap().push(("spawn_into".to_string(), argv.to_vec()));
47
+ Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid: None })
48
+ }
49
+ fn inject(&self, _t: &crate::transport::Target, _p: &crate::transport::InjectPayload, _s: crate::transport::Key, _b: bool) -> Result<crate::transport::InjectReport, crate::transport::TransportError> {
50
+ unimplemented!("LaneTransport::inject not reached by stop/reset/remove/fork")
51
+ }
52
+ fn send_keys(&self, _t: &crate::transport::Target, _k: &[crate::transport::Key]) -> Result<(), crate::transport::TransportError> {
53
+ Ok(())
54
+ }
55
+ fn capture(&self, _t: &crate::transport::Target, r: crate::transport::CaptureRange) -> Result<crate::transport::CapturedText, crate::transport::TransportError> {
56
+ Ok(crate::transport::CapturedText { text: String::new(), range: r })
57
+ }
58
+ fn query(&self, _t: &crate::transport::Target, _f: crate::transport::PaneField) -> Result<Option<String>, crate::transport::TransportError> {
59
+ Ok(None)
60
+ }
61
+ fn liveness(&self, _p: &crate::transport::PaneId) -> Result<crate::model::enums::PaneLiveness, crate::transport::TransportError> {
62
+ Ok(crate::model::enums::PaneLiveness::Unknown)
63
+ }
64
+ fn list_targets(&self) -> Result<Vec<crate::transport::PaneInfo>, crate::transport::TransportError> {
65
+ Ok(self
66
+ .windows
67
+ .iter()
68
+ .map(|w| crate::transport::PaneInfo {
69
+ pane_id: crate::transport::PaneId::new(format!("%{w}")),
70
+ session: crate::transport::SessionName::new(&self.session),
71
+ window_index: None,
72
+ window_name: Some(crate::transport::WindowName::new(w)),
73
+ pane_index: None,
74
+ tty: None,
75
+ current_command: None,
76
+ current_path: None,
77
+ active: false,
78
+ pane_pid: None,
79
+ leader_env: std::collections::BTreeMap::new(),
80
+ })
81
+ .collect())
82
+ }
83
+ fn has_session(&self, _s: &crate::transport::SessionName) -> Result<bool, crate::transport::TransportError> {
84
+ Ok(true)
85
+ }
86
+ fn list_windows(&self, s: &crate::transport::SessionName) -> Result<Vec<crate::transport::WindowName>, crate::transport::TransportError> {
87
+ if s.as_str() == self.session {
88
+ Ok(self.windows.iter().map(|w| crate::transport::WindowName::new(w.as_str())).collect())
89
+ } else {
90
+ Ok(Vec::new())
91
+ }
92
+ }
93
+ fn set_session_env(&self, _s: &crate::transport::SessionName, _k: &str, _v: &str) -> Result<crate::transport::SetEnvOutcome, crate::transport::TransportError> {
94
+ Ok(crate::transport::SetEnvOutcome::Applied)
95
+ }
96
+ fn kill_session(&self, _s: &crate::transport::SessionName) -> Result<(), crate::transport::TransportError> {
97
+ Ok(())
98
+ }
99
+ fn kill_window(&self, t: &crate::transport::Target) -> Result<(), crate::transport::TransportError> {
100
+ let name = match t {
101
+ crate::transport::Target::Pane(p) => p.as_str().to_string(),
102
+ crate::transport::Target::SessionWindow { session, window } => format!("{}:{}", session.as_str(), window.as_str()),
103
+ };
104
+ self.killed.lock().unwrap().push(name);
105
+ Ok(())
106
+ }
107
+ fn attach_session(&self, _s: &crate::transport::SessionName) -> Result<crate::transport::AttachOutcome, crate::transport::TransportError> {
108
+ Ok(crate::transport::AttachOutcome::Attached)
109
+ }
110
+ }
111
+
112
+ /// 2-agent (alpha, bravo) compiled spec + custom `state.agents` map. session_name = "team-laneateam"
113
+ /// (the compiled runtime.session_name). ensure_owner_allowed passes (no team_owner).
114
+ fn lanea_ws_agents(agents: serde_json::Value) -> PathBuf {
115
+ let ws = temp_ws().join("laneav2");
116
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
117
+ std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A v2 probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
118
+ std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
119
+ std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
120
+ let spec = crate::compiler::compile_team(&ws).expect("compile lane-A v2 team");
121
+ // Make `alpha` REMOVABLE (golden-valid): compile_team auto-wires EVERY agent into routing/tasks
122
+ // (default_assignee + route-<id>.assign_to + task.assignee), but golden remove_agent removes only from
123
+ // agents + startup_order then validate_spec RAISES on dangling refs (agents.py:94 / spec.py:341-346) —
124
+ // so a routed agent is NOT removable in golden. Re-point the *validated* refs at the STAYING agent
125
+ // `bravo`, so removing `alpha` (an unrouted, dynamic-style worker — the real fork->remove case) passes
126
+ // validate. (match-block `assignee` lists are not validated, so they may keep referencing alpha.)
127
+ let yaml = crate::model::yaml::dumps(&spec)
128
+ .replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
129
+ .replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
130
+ .replace("assignee: \"alpha\"", "assignee: \"bravo\"");
131
+ assert!(!yaml.contains("default_assignee: \"alpha\""), "fixture unroute: default_assignee still alpha");
132
+ assert!(!yaml.contains("assign_to: \"alpha\""), "fixture unroute: a routing rule still assign_to alpha");
133
+ assert!(!yaml.contains("assignee: \"alpha\""), "fixture unroute: task still assignee alpha");
134
+ std::fs::write(ws.join("team.spec.yaml"), yaml).unwrap();
135
+ crate::state::persist::save_runtime_state(&ws, &json!({ "session_name": "team-laneateam", "agents": agents })).unwrap();
136
+ ws
137
+ }
138
+
139
+ /// SINGLE-worker compiled spec (alpha only). Removing alpha leaves `agents: []` -> validate_spec FAILS
140
+ /// ("/agents: must be a non-empty list", spec.rs:273) -> a deterministic IN-TRY mid-remove failure that
141
+ /// drives the rollback path (golden agents.py:110 except).
142
+ fn lanea_one_agent_ws(alpha_status: &str) -> PathBuf {
143
+ let ws = temp_ws().join("lanea1");
144
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
145
+ std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A one-agent probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
146
+ std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
147
+ let spec = crate::compiler::compile_team(&ws).expect("compile 1-agent team");
148
+ std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
149
+ crate::state::persist::save_runtime_state(&ws, &json!({ "session_name": "team-laneateam", "agents": { "alpha": { "status": alpha_status, "provider": "codex", "window": "alpha" } } })).unwrap();
150
+ ws
151
+ }
152
+
153
+ /// Rewrite the compiled spec's `context.state_file` (default `team_state.md`) to `name`, so the
154
+ /// rollback team-state path divergence (capture honors spec.context.state_file; restore hardcodes
155
+ /// team_state.md) is observable. Asserts the rewrite took, so a format change can't silently mis-test.
156
+ fn set_context_state_file(ws: &std::path::Path, name: &str) {
157
+ let p = ws.join("team.spec.yaml");
158
+ let text = std::fs::read_to_string(&p).unwrap();
159
+ let replaced = text.replace("state_file: \"team_state.md\"", &format!("state_file: \"{name}\""));
160
+ assert_ne!(replaced, text, "expected to rewrite context.state_file in the compiled spec; got:\n{text}");
161
+ std::fs::write(&p, replaced).unwrap();
162
+ }
163
+
164
+ /// Fork workspace: source `alpha` (role doc = `alpha_role`) + `bravo`, alpha seeded RUNNING with a
165
+ /// source `session_id` (so fork reaches the spec-mutation / native-fork gate). Seeded-healthy coordinator
166
+ /// (fork's start_coordinator -> AlreadyRunning, no real daemon). session_name = "team-laneateam".
167
+ fn fork_ws(alpha_role: &str) -> PathBuf {
168
+ let ws = temp_ws().join("forkv2");
169
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
170
+ std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Fork v2 probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
171
+ std::fs::write(ws.join("agents").join("alpha.md"), alpha_role).unwrap();
172
+ std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
173
+ let spec = crate::compiler::compile_team(&ws).expect("compile fork team");
174
+ std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
175
+ crate::state::persist::save_runtime_state(
176
+ &ws,
177
+ &json!({
178
+ "session_name": "team-laneateam",
179
+ "agents": {
180
+ "alpha": { "status": "running", "provider": "codex", "window": "alpha", "session_id": "sess-a" },
181
+ "bravo": { "status": "running", "provider": "codex", "window": "bravo" }
182
+ }
183
+ }),
184
+ )
185
+ .unwrap();
186
+ seed_healthy_coordinator(&ws);
187
+ ws
188
+ }
189
+
190
+ // ── STOP #1 (stop-window-gate-1) [RED] — window ABSENT => stopped=false + NO kill ────────────────────
191
+ // Golden operations.py:81-99 gates `tmux kill-window` behind `if _tmux_window_exists(session, window)`;
192
+ // an absent window (already-stopped / never-started spec-known agent) skips the kill and returns
193
+ // {ok:true, status:"stopped", stopped:FALSE}. Rust kills UNCONDITIONALLY (restart.rs:181-183) and
194
+ // hardcodes stopped:true (:192). RED: LaneTransport with NO windows -> the porter's list_windows gate
195
+ // must skip the kill (killed empty) and set stopped=false; today kill_window IS called + stopped=true.
196
+ #[test]
197
+ fn lanea_stop_window_absent_returns_stopped_false_no_kill() {
198
+ let ws = lanea_ws_agents(json!({ "alpha": { "status": "running", "provider": "codex", "window": "alpha" } }));
199
+ let tx = LaneTransport::new("team-laneateam", &[]); // alpha's window is ABSENT
200
+ let report = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("window-absent stop is a clean Ok, not an Err (golden returns stopped:false)");
201
+ assert!(
202
+ !report.stopped,
203
+ "golden operations.py:81-99: an ABSENT tmux window => stopped=FALSE (kill skipped); Rust hardcodes stopped:true"
204
+ );
205
+ assert!(
206
+ tx.killed().is_empty(),
207
+ "golden gates kill on _tmux_window_exists; an absent window must NOT be killed; Rust kills unconditionally, killed={:?}",
208
+ tx.killed()
209
+ );
210
+ }
211
+
212
+ // ── STOP #1 companion [LOCK] — window PRESENT => stopped=true + kill the golden target ───────────────
213
+ // The OTHER side of the gate: when the window exists, golden kills `<session>:<window>` and returns
214
+ // stopped:true. GREEN today (Rust kills unconditionally), so this LOCKS the present-branch against the
215
+ // window-gate fix regressing it.
216
+ #[test]
217
+ fn lanea_stop_window_present_kills_and_stopped_true() {
218
+ let ws = lanea_ws_agents(json!({ "alpha": { "status": "running", "provider": "codex", "window": "alpha" } }));
219
+ let tx = LaneTransport::new("team-laneateam", &["alpha"]); // window present
220
+ let report = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("present-window stop ok");
221
+ assert!(report.stopped, "a present window must be killed => stopped=true");
222
+ assert_eq!(
223
+ tx.killed(),
224
+ vec!["team-laneateam:alpha".to_string()],
225
+ "stop must kill exactly the golden target <session>:<window>; killed={:?}",
226
+ tx.killed()
227
+ );
228
+ }
229
+
230
+ // ── STOP #2 (stop-display-noop-2) [RED] — close ghostty_workspace slot: persist display.status/pane_title
231
+ // Golden operations.py:88-92 -> display/close.py:84-85 relabels the slot: display["status"]="stopped",
232
+ // display["pane_title"]=f"stopped: {agent_id}", written back into the persisted agent entry. Rust never
233
+ // touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
234
+ // persisted display.status/pane_title are the in-process observable.
235
+ #[test]
236
+ fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
237
+ let ws = lanea_ws_agents(json!({
238
+ "alpha": { "status": "running", "provider": "codex", "window": "alpha",
239
+ "display": { "backend": "ghostty_workspace", "pane_id": "%5", "linked_session": "disp-alpha", "status": "running", "pane_title": "alpha" } }
240
+ }));
241
+ let tx = LaneTransport::new("team-laneateam", &["alpha"]);
242
+ let _ = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("stop ok");
243
+ let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
244
+ assert_eq!(
245
+ state.pointer("/agents/alpha/display/status").and_then(serde_json::Value::as_str),
246
+ Some("stopped"),
247
+ "stop must relabel a ghostty_workspace slot: display.status='stopped' (close.py:84); Rust leaves it 'running'"
248
+ );
249
+ assert_eq!(
250
+ state.pointer("/agents/alpha/display/pane_title").and_then(serde_json::Value::as_str),
251
+ Some("stopped: alpha"),
252
+ "stop must set display.pane_title='stopped: <id>' (close.py:85); Rust never touches the display"
253
+ );
254
+ }
255
+
256
+ // ── RESET #3 (reset-paused-restart-2) [RED] — reset of a PAUSED agent returns ok=true (NOT an Err) ───
257
+ // Golden operations.py:126-140: after discard, reset re-spawns via start_agent(force,allow_fresh).
258
+ // discard does NOT clear `paused`, so start_agent returns the refusal-shaped {ok:False,status:paused,
259
+ // reason:agent_paused} (start.py:101) WITHOUT raising; reset embeds it as `started` and returns the
260
+ // success envelope {ok:True, status:"running", started:{ok:False,...}, coordinator:None}. Rust maps
261
+ // StartAgentOutcome::Paused -> Err(RequirementUnmet "agent ... is paused") (restart.rs:251-253) — a hard
262
+ // error instead of golden's ok=true. RED: reset of a paused agent must be Ok, not Err.
263
+ #[test]
264
+ fn lanea_reset_paused_agent_returns_ok_not_err() {
265
+ let ws = lanea_ws_agents(json!({
266
+ "alpha": { "status": "running", "provider": "codex", "window": "alpha", "paused": true, "session_id": "sess-a" }
267
+ }));
268
+ let tx = LaneTransport::new("team-laneateam", &["alpha"]);
269
+ let result = reset_agent_with_transport(&ws, &aid("alpha"), true, false, None, &tx);
270
+ assert!(
271
+ result.is_ok(),
272
+ "golden operations.py:133-140: reset of a PAUSED agent returns the ok=true success envelope embedding \
273
+ started={{ok:false,status:paused,reason:agent_paused}}; Rust raises RequirementUnmet. Porter must add a \
274
+ ResetAgentOutcome variant carrying the paused `started` result. got {result:?}"
275
+ );
276
+ assert!(
277
+ !matches!(result, Ok(ResetAgentOutcome::Refused { .. })),
278
+ "discard_session=true was passed -> the DiscardSessionRequired refusal must NOT fire; got {result:?}"
279
+ );
280
+ }
281
+
282
+ // ── REMOVE #4 (remove-dynamic-agent-refused-1) [RED] — dynamic agent removable WITHOUT from_spec ─────
283
+ // Golden agents.py:50-54: dynamic_agent = bool(agent_state.dynamic_role_file OR agent.forked_from);
284
+ // it only refuses when `not dynamic_agent and not (from_spec and confirm)`. A dynamic/forked agent is
285
+ // removable with from_spec=false. Rust unconditionally `if !from_spec -> RefusedFromSpecConfirm`
286
+ // (restart.rs:285) BEFORE even loading the spec/state -> wrongly refuses the dynamic agent. RED.
287
+ #[test]
288
+ fn lanea_remove_dynamic_agent_removable_without_from_spec() {
289
+ let ws = lanea_ws_agents(json!({
290
+ "alpha": { "status": "stopped", "provider": "codex", "window": "alpha", "dynamic_role_file": ".team/dynamic-role-files/alpha.md" },
291
+ "bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
292
+ }));
293
+ // make the dynamic role file exist (so removal resolves+deletes it cleanly under either path policy).
294
+ let dyn_dir = ws.join(".team").join("dynamic-role-files");
295
+ std::fs::create_dir_all(&dyn_dir).unwrap();
296
+ std::fs::write(dyn_dir.join("alpha.md"), "dynamic alpha role\n").unwrap();
297
+ let tx = LaneTransport::new("team-laneateam", &[]); // alpha not running
298
+ let result = remove_agent_with_transport(&ws, &aid("alpha"), false, true, None, &tx); // from_spec=FALSE
299
+ assert!(
300
+ !matches!(result, Ok(RemoveAgentOutcome::RefusedFromSpecConfirm { .. })),
301
+ "golden agents.py:50-54: a DYNAMIC agent (state.dynamic_role_file) is removable with from_spec=false; \
302
+ Rust wrongly returns RefusedFromSpecConfirm; got {result:?}"
303
+ );
304
+ let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
305
+ assert!(
306
+ state.get("agents").and_then(serde_json::Value::as_object).is_some_and(|a| !a.contains_key("alpha")),
307
+ "the dynamic agent must actually be removed from state.agents; got {result:?}"
308
+ );
309
+ }
310
+
311
+ // ── REMOVE #5 (remove-unknown-precedence-2) [RED] — unknown-worker raised BEFORE from_spec refusal ───
312
+ // Golden agents.py:41-54 loads the spec and runs _find_worker (raising "unknown worker agent id: <id>")
313
+ // BEFORE the from_spec/confirm refusal at :53. Rust checks `!from_spec` FIRST (restart.rs:285) and
314
+ // returns RefusedFromSpecConfirm without ever loading the spec -> a nonexistent agent is mis-reported as
315
+ // a from_spec refusal. RED: an unknown agent with from_spec=false must surface "unknown worker".
316
+ #[test]
317
+ fn lanea_remove_unknown_agent_precedes_from_spec_refusal() {
318
+ let ws = lanea_ws_agents(json!({ "alpha": { "status": "stopped", "provider": "codex", "window": "alpha" } }));
319
+ let tx = LaneTransport::new("team-laneateam", &[]);
320
+ let text = format!("{:?}", remove_agent_with_transport(&ws, &aid("ghost"), false, false, None, &tx));
321
+ assert!(
322
+ text.contains("unknown worker"),
323
+ "golden agents.py:41-54: the unknown-worker check precedes the from_spec refusal; an unknown agent must \
324
+ raise 'unknown worker agent id: ghost', NOT RefusedFromSpecConfirm; got {text}"
325
+ );
326
+ }
327
+
328
+ // ── REMOVE #8 (remove-team-state-md-content-5) [RED] — team_state.md is golden MARKDOWN, not JSON ────
329
+ // Golden state.py:625-686 write_team_state builds a Markdown doc ("# Team State", "## Objective",
330
+ // "## Agents" with one "- {id}: {role} on {provider} ({status})" per spec agent, etc.) from removed_spec.
331
+ // Rust write_team_state (restart.rs:925-941) writes serde_json::to_string_pretty(state) — raw JSON — and
332
+ // passes the ORIGINAL spec (so the removed agent would not be excluded). RED: after a successful remove,
333
+ // team_state.md must be the Markdown doc, list the REMAINING agent (bravo) and NOT the removed one.
334
+ #[test]
335
+ fn lanea_remove_writes_markdown_team_state_not_json() {
336
+ let ws = lanea_ws_agents(json!({
337
+ "alpha": { "status": "stopped", "provider": "codex", "window": "alpha" },
338
+ "bravo": { "status": "running", "provider": "codex", "window": "bravo" }
339
+ }));
340
+ let tx = LaneTransport::new("team-laneateam", &[]);
341
+ let _ = remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx).expect("remove ok");
342
+ let team_state = std::fs::read_to_string(ws.join("team_state.md")).expect("team_state.md written");
343
+ assert!(
344
+ team_state.starts_with("# Team State"),
345
+ "golden write_team_state emits a Markdown document starting '# Team State'; Rust dumps JSON; got:\n{team_state}"
346
+ );
347
+ assert!(
348
+ !team_state.trim_start().starts_with('{'),
349
+ "team_state.md must NOT be a JSON dump of runtime state; got:\n{team_state}"
350
+ );
351
+ assert!(team_state.contains("## Agents"), "golden has a '## Agents' section; got:\n{team_state}");
352
+ assert!(
353
+ team_state.contains("bravo: Bravo Worker on codex"),
354
+ "the '## Agents' section must list the remaining agent bravo (golden '- {{id}}: {{role}} on {{provider}} ({{status}})'); got:\n{team_state}"
355
+ );
356
+ assert!(
357
+ !team_state.contains("alpha: Alpha Worker"),
358
+ "the removed agent (alpha) must be EXCLUDED — golden writes removed_spec, not the original spec; got:\n{team_state}"
359
+ );
360
+ }
361
+
362
+ // ── REMOVE #10 (remove-is-running-no-tmux-fallback-7) [RED] — is_running honors the tmux-window fallback
363
+ // Golden agents.py:247-252 _is_running returns True if status in {running,busy} OR
364
+ // (session_name AND _tmux_window_exists(session, window)). Rust agent_is_running (restart.rs:689-700)
365
+ // only checks status -> an agent with a STALE status ('idle') whose tmux window is still live is treated
366
+ // as not-running, so removal without --force is wrongly ALLOWED. RED: such an agent removed without
367
+ // force must be RefusedForceRequired (golden), not Removed.
368
+ #[test]
369
+ fn lanea_remove_is_running_honors_tmux_window_fallback() {
370
+ let ws = lanea_ws_agents(json!({
371
+ "alpha": { "status": "idle", "provider": "codex", "window": "alpha" }, // stale status, but window is live
372
+ "bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
373
+ }));
374
+ let tx = LaneTransport::new("team-laneateam", &["alpha"]); // alpha's tmux window EXISTS
375
+ let result = remove_agent_with_transport(&ws, &aid("alpha"), true, false, None, &tx); // force=FALSE
376
+ assert!(
377
+ matches!(result, Ok(RemoveAgentOutcome::RefusedForceRequired { .. })),
378
+ "golden agents.py:247-252: a stale-status agent whose tmux window is LIVE counts as running -> removal \
379
+ without --force is RefusedForceRequired; Rust drops the tmux fallback and allows it. Porter must thread \
380
+ the transport into agent_is_running. got {result:?}"
381
+ );
382
+ }
383
+
384
+ // ── REMOVE #7 (remove-rollback-no-restart-running-4) [RED] — rollback RESTARTS the force-stopped worker
385
+ // Golden agents.py:78,219-223: after force-stopping a running worker, rollback (on any in-try failure)
386
+ // sets restore_running and calls start_agent(force=True, allow_fresh=True) to bring the worker back.
387
+ // Rust rollback (restart.rs:1023-1067) has no restore_running and never restarts -> a force-remove that
388
+ // stops a running agent then fails leaves it DEAD. RED: drive a deterministic in-try failure
389
+ // (1-worker team: removing alpha -> empty agents -> validate_spec fails AFTER the force-stop) and assert
390
+ // the transport recorded a re-spawn during rollback (the golden worker restart). Today: zero spawns.
391
+ #[test]
392
+ fn lanea_remove_rollback_restarts_force_stopped_worker() {
393
+ let ws = lanea_one_agent_ws("running"); // removing alpha -> agents:[] -> validate_spec FAILS post-stop
394
+ let tx = LaneTransport::new("team-laneateam", &["alpha"]);
395
+ let result = remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx); // from_spec+force
396
+ assert!(
397
+ result.is_err(),
398
+ "precondition: removing the only worker makes removed_spec invalid (validate_spec) -> the remove fails \
399
+ after the force-stop, triggering rollback; got {result:?}"
400
+ );
401
+ assert!(
402
+ tx.killed().contains(&"team-laneateam:alpha".to_string()),
403
+ "precondition: the running worker was force-stopped (window killed) before the failure; killed={:?}",
404
+ tx.killed()
405
+ );
406
+ assert!(
407
+ !tx.spawns().is_empty(),
408
+ "golden agents.py:219-223: rollback must RESTART the force-stopped worker (start_agent force=True) -> a \
409
+ re-spawn; Rust rollback never restarts, leaving the worker dead. Porter must thread the transport into \
410
+ RemoveRollback::restore. spawns={:?}",
411
+ tx.spawns()
412
+ );
413
+ }
414
+
415
+ // ── REMOVE #11 (remove-dynamic-role-path-and-required-8, warn) [RED] — missing REQUIRED role file raises
416
+ // Golden agents.py:255-261 _remove_dynamic_role_file(path, required=True) RAISES "dynamic role file
417
+ // missing: <path>" when the state recorded a dynamic_role_file but it is absent. Rust hardcodes the
418
+ // default path and returns Ok(false) silently (restart.rs:951-953), losing the hard-fail+rollback. RED:
419
+ // a dynamic agent whose recorded role file is MISSING must raise, not silently complete the removal.
420
+ #[test]
421
+ fn lanea_remove_dynamic_role_file_missing_raises() {
422
+ let ws = lanea_ws_agents(json!({
423
+ "alpha": { "status": "stopped", "provider": "codex", "window": "alpha", "dynamic_role_file": ".team/dynamic-role-files/custom.md" }, // file NOT created
424
+ "bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
425
+ }));
426
+ let tx = LaneTransport::new("team-laneateam", &[]);
427
+ let text = format!("{:?}", remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx));
428
+ assert!(
429
+ text.contains("dynamic role file missing"),
430
+ "golden agents.py:259-260: a state-recorded dynamic_role_file that is MISSING must RAISE 'dynamic role \
431
+ file missing: <path>' (required=true); Rust returns Ok(false) silently and completes the remove. got {text}"
432
+ );
433
+ }
434
+
435
+ // ── REMOVE #9 (remove-rollback-team-state-path-6) [RED] — rollback restores via spec.context.state_file
436
+ // Golden agents.py:181-204 derives team_state_path = workspace / spec.context.state_file once and uses
437
+ // it for BOTH capture and restore. Rust capture honors spec.context.state_file (restart.rs:994-999) but
438
+ // restore HARDCODES workspace/team_state.md (:1031). With a custom state_file, rollback writes the
439
+ // captured content to the WRONG file. RED (deterministic in-try failure via the 1-worker validate-fail):
440
+ // after rollback, a SPURIOUS team_state.md must NOT exist (golden restores the custom file, never creates
441
+ // team_state.md). Today the hardcoded restore creates team_state.md.
442
+ #[test]
443
+ fn lanea_remove_rollback_restores_via_spec_state_file_path() {
444
+ let ws = lanea_one_agent_ws("stopped");
445
+ set_context_state_file(&ws, "custom_state.md");
446
+ std::fs::write(ws.join("custom_state.md"), "ORIGINAL CUSTOM TEAM STATE\n").unwrap(); // capture reads this
447
+ let tx = LaneTransport::new("team-laneateam", &[]);
448
+ let result = remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx);
449
+ assert!(result.is_err(), "precondition: 1-worker removal -> validate_spec fails -> rollback runs; got {result:?}");
450
+ assert!(
451
+ !ws.join("team_state.md").exists(),
452
+ "golden agents.py:200-204: rollback restores the spec-derived state_file (custom_state.md), it must NOT \
453
+ create the hardcoded team_state.md. Porter must capture team_state_path on the rollback struct and reuse \
454
+ it in restore."
455
+ );
456
+ }
457
+
458
+ // ── FORK (fork-dup-guard-misses-leader) [RED] — forking ONTO the leader id is 'already exists' ───────
459
+ // Golden operations.py:301-302 uses _find_agent (matches agents AND the leader, runtime.py:1055), so
460
+ // forking to as_agent_id == leader.id raises 'agent id already exists: <id>'. Rust find_spec_agent
461
+ // short-circuits to None for the leader id (launch.rs:507-515) -> the duplicate guard is SKIPPED and the
462
+ // fork proceeds against the leader id. RED: fork target == "leader" must be 'already exists'.
463
+ #[test]
464
+ fn lanea_fork_dup_target_leader_id_is_already_exists() {
465
+ let ws = fork_ws(DELEG_ROLE_ALPHA);
466
+ let tx = LaneTransport::new("team-laneateam", &[]);
467
+ let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), false, None, &tx));
468
+ assert!(
469
+ text.contains("already exists"),
470
+ "golden operations.py:301-302 (_find_agent matches the leader): forking ONTO the leader id must raise \
471
+ 'agent id already exists: leader'; Rust skips the dup guard for the leader id and proceeds. got {text}"
472
+ );
473
+ }
474
+
475
+ // ── FORK (fork-missing-tmux-window-guard) [RED] — window-already-exists guard BEFORE spec mutation ────
476
+ // Golden operations.py:310-312: after the session_id guard and BEFORE mutating the spec, raise
477
+ // 'tmux window already exists for fork target: {session}:{as_agent_id}' if the window exists. Rust has no
478
+ // such guard (launch.rs:439-440) -> it appends + writes the spec regardless. RED: a pre-existing window
479
+ // for the target must (a) raise that exact message and (b) leave the spec UNMUTATED (no fork agent).
480
+ #[test]
481
+ fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
482
+ let ws = fork_ws(DELEG_ROLE_ALPHA);
483
+ let tx = LaneTransport::new("team-laneateam", &["newfork"]); // the target window already exists
484
+ let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx));
485
+ assert!(
486
+ text.contains("tmux window already exists for fork target: team-laneateam:newfork"),
487
+ "golden operations.py:310-312: a pre-existing target window must raise 'tmux window already exists for \
488
+ fork target: team-laneateam:newfork' BEFORE spec mutation; Rust has no guard. got {text}"
489
+ );
490
+ let spec_text = std::fs::read_to_string(ws.join("team.spec.yaml")).unwrap();
491
+ assert!(
492
+ !spec_text.contains("newfork"),
493
+ "the guard must fire BEFORE the spec is mutated; the spec must NOT contain the fork agent 'newfork'"
494
+ );
495
+ }
496
+
497
+ // ── FORK (fork-gate-error-text) [RED] + (fork-incomplete-rollback, adapter arm) — golden gate text + spec rollback
498
+ // Golden operations.py:329-330 raises f"{provider} does not support native session fork" when the native
499
+ // fork gate fails (auth_mode==compatible_api). Rust relies on adapter.fork() -> CapabilityUnsupported
500
+ // ("Codex:fork") (adapter.rs:310) -> a different observable. AND golden wraps the post-spec-write steps
501
+ // in try/except restoring the spec on ANY failure (operations.py:384-394); Rust writes the spec
502
+ // (launch.rs:443) then errors at adapter.fork (458-460) WITHOUT restoring it. RED on both: the message
503
+ // text AND the spec must be rolled back to not contain the fork agent.
504
+ #[test]
505
+ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
506
+ let ws = fork_ws(DELEG_ROLE_ALPHA_COMPAT); // source alpha auth_mode=compatible_api -> native fork unsupported
507
+ let tx = LaneTransport::new("team-laneateam", &[]);
508
+ let result = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx);
509
+ let text = format!("{result:?}");
510
+ assert!(
511
+ text.contains("codex does not support native session fork"),
512
+ "golden operations.py:329-330: the native-fork gate must raise 'codex does not support native session \
513
+ fork'; Rust surfaces the generic 'capability unsupported: Codex:fork'. got {text}"
514
+ );
515
+ let spec_text = std::fs::read_to_string(ws.join("team.spec.yaml")).unwrap();
516
+ assert!(
517
+ !spec_text.contains("newfork"),
518
+ "golden operations.py:384-394: on the gate failure the spec must be ROLLED BACK; Rust writes the spec \
519
+ then errors at adapter.fork without restoring it, leaving the fork agent 'newfork' in the spec"
520
+ );
521
+ }
522
+
523
+ // ── FORK (fork-report-session-id-is-pane-id) [RED] — report session_id is the captured id / None, not pane
524
+ // Golden operations.py:399,408 returns state['agents'][as_agent_id].get('session_id') — the captured
525
+ // provider session id (or None if capture missed, raise_on_missed=False). Rust sets
526
+ // session_id: Some(SessionId::new(spawn.pane_id)) (launch.rs:502) — the tmux pane id ('%newfork'),
527
+ // a different value kind. RED: the report session_id must NOT be the pane id (None, since the Rust fork
528
+ // path performs no session capture).
529
+ #[test]
530
+ fn lanea_fork_report_session_id_is_not_pane_id() {
531
+ let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
532
+ let tx = LaneTransport::new("team-laneateam", &[]);
533
+ let report = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx).expect("fork ok (codex subscription supports fork)");
534
+ assert_ne!(
535
+ report.session_id,
536
+ Some(crate::provider::SessionId::new("%newfork")),
537
+ "golden operations.py:399,408: report.session_id is the captured provider session id / None, NEVER the \
538
+ tmux pane id; Rust returns Some(pane_id='%newfork')"
539
+ );
540
+ assert_eq!(
541
+ report.session_id, None,
542
+ "the Rust fork path captures no session -> report.session_id must be None (golden capture-missed), not the pane id"
543
+ );
544
+ }
545
+
546
+ // ── REMOVE #6/#12 (remove-rollback-no-agent-health-3 / remove-rollback-health-1) [SEAM #[ignore]] ────
547
+ // Golden _RemoveRollback captures `self.health = copy.deepcopy(store.agent_health().get(agent_id))`
548
+ // (agents.py:185) and restore() re-upserts it via _restore_agent_health (agents.py:215-218,268-278). The
549
+ // Rust RemoveRollback has NO health field and never restores it (restart.rs:972-1067). This is only
550
+ // observable when a step AFTER the agent_health delete fails — but Rust's only post-delete step is the
551
+ // snapshot, which golden runs OUTSIDE the rollback-protected region (agents.py:135). Exercising it
552
+ // golden-faithfully needs a production failure-injection seam at an in-try step after the delete (mirror
553
+ // the coordinator SaveHook). PORTER: add `health: Option<Value>` to RemoveRollback (capture the row
554
+ // before delete; restore re-upserts status||"IDLE"/last_output_at/context_usage_pct/current_task_id, or
555
+ // deletes if None) AND move save_team_runtime_snapshot OUTSIDE the rollback region (golden agents.py:135).
556
+ #[test]
557
+ #[ignore = "seam: agent_health rollback restore needs a failure-injection hook (post-delete, in-try) to \
558
+ exercise in-process; golden agents.py:185/215-218/268-278. Porter adds RemoveRollback.health + \
559
+ moves save_team_runtime_snapshot outside the rollback region."]
560
+ fn lanea_remove_rollback_restores_agent_health() {
561
+ // Golden contract (verified by reading agents.py): on a mid-remove failure after the agent_health
562
+ // row is deleted, rollback re-upserts the captured row so the health history is not lost.
563
+ }
564
+
565
+ // ── FORK (fork-incomplete-rollback) [SEAM #[ignore]] — post-spawn rollback arms ─────────────────────
566
+ // Golden operations.py:384-394 wraps spec-mutation..start_coordinator in try/except; on ANY failure it
567
+ // (1) kills the spawned tmux window if present, (2) adapter.cleanup_mcp, (3) restores old spec text, and
568
+ // (4) restores prior state. Rust only restores the spec on the spawn_into arm (launch.rs:481); the
569
+ // save_runtime_state (486-487) and start_coordinator (488-493) failure arms leave the spec mutated, the
570
+ // already-spawned window un-killed, and the state un-rolled-back; install_mcp/cleanup_mcp are absent.
571
+ // The adapter.fork arm IS covered HARD above (lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm).
572
+ // The post-SPAWN arms need a failure-injection seam after spawn_into (codex+subscription forks past
573
+ // adapter.fork, so the spawn succeeds and there is no in-process way to fail save/coordinator cleanly).
574
+ // PORTER: a Drop guard armed after the spec write, disarmed on success — kills the window, restores spec
575
+ // + state, runs cleanup_mcp on every post-write error arm.
576
+ #[test]
577
+ #[ignore = "seam: fork post-spawn rollback arms (save_runtime_state / start_coordinator failure) need a \
578
+ failure-injection hook after spawn_into; golden operations.py:384-394. Porter wires a Drop \
579
+ guard (kill window + restore spec/state + cleanup_mcp) armed after the spec write."]
580
+ fn lanea_fork_rollback_complete_on_post_spawn_failure() {
581
+ // Golden contract (operations.py:384-394): a post-spawn failure kills the spawned window, restores
582
+ // the old spec text + prior state, and runs cleanup_mcp before re-raising.
583
+ }