@team-agent/installer 0.2.11 → 0.3.1

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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -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 +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
  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 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  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 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  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 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  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 -503
  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 -91
  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 -1243
  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 -144
  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 -693
  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,410 @@
1
+ use super::*;
2
+
3
+ // = RED. The porter must reproduce the EXACT LeaseResult shape per outcome.
4
+ // We seed state.json directly and inject a PaneLivenessProbe so no real tmux
5
+ // is needed for the no-pane / live-owner / dead-owner gate decisions.
6
+
7
+ /// Injectable liveness probe (`state::owner_gate::PaneLivenessProbe`): a pane
8
+ /// in the seeded set is `Live`, otherwise `Dead`. The porter's real probe
9
+ /// (step 9 tmux) replaces it; here it drives the no-incident lease gate
10
+ /// (bound-pane liveness + caller eligibility) deterministically with no tmux.
11
+ #[test]
12
+ #[serial_test::serial(env)]
13
+ fn claim_lease_no_incident_vacant_acquire_advances_epoch_and_dual_writes() {
14
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
15
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
16
+ let ws = p2_temp_ws("claim_vacant");
17
+ let team_id = TeamKey::new("current");
18
+ let caller = PaneId::new("%5");
19
+ // empty (vacant) state with a session so dual-state writes both files.
20
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
21
+ let event_log = crate::event_log::EventLog::new(&ws);
22
+ let live = seeded_liveness(&["%5"]);
23
+ let r = claim_lease_no_incident(
24
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
25
+ )
26
+ .unwrap();
27
+ assert!(r.ok);
28
+ assert_eq!(r.status, LeaseStatus::Claimed);
29
+ assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
30
+ assert_eq!(r.owner_epoch, Some(OwnerEpoch(1)), "vacant acquire 0→1");
31
+ let owner = r.owner.as_ref().expect("claimed → owner");
32
+ assert_eq!(owner.pane_id, caller);
33
+ assert_eq!(owner.owner_epoch, OwnerEpoch(1));
34
+ assert_eq!(owner.claimed_via, ClaimedVia::ClaimLeader);
35
+ let receiver = r.receiver.as_ref().expect("claimed → receiver");
36
+ assert_eq!(receiver.pane_id, caller);
37
+ assert_eq!(receiver.discovery, Some(Discovery::ClaimLeader));
38
+ // dual-state: BOTH workspace state.json + team/<session> snapshot carry %5/epoch 1.
39
+ let ws_path = crate::state::persist::runtime_state_path(&ws);
40
+ let ws_state: serde_json::Value =
41
+ serde_json::from_str(&std::fs::read_to_string(&ws_path).unwrap()).unwrap();
42
+ assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%5"));
43
+ assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(1));
44
+ let snap_path = crate::model::paths::runtime_dir(&ws)
45
+ .join("teams").join("team-agent-x").join("state.json");
46
+ let snap: serde_json::Value =
47
+ serde_json::from_str(&std::fs::read_to_string(&snap_path).unwrap()).unwrap();
48
+ assert_eq!(snap["team_owner"]["pane_id"], serde_json::json!("%5"), "dual-state snapshot owner");
49
+ assert_eq!(snap["team_owner"]["owner_epoch"], serde_json::json!(1));
50
+ }
51
+
52
+ // RED — DEAD-OWNER RECOVER: bound pane %1 absent from live set, caller %5
53
+ // eligible, confirm=false → still acquires (dead owner never blocks),
54
+ // reason=previous_owner_pane_dead, epoch 3→4. golden /tmp/probe_confirm.py.
55
+ #[test]
56
+ #[serial_test::serial(env)]
57
+ fn claim_lease_no_incident_dead_owner_recovers_without_confirm() {
58
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
59
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
60
+ let ws = p2_temp_ws("claim_dead");
61
+ let team_id = TeamKey::new("current");
62
+ let caller = PaneId::new("%5");
63
+ let mut state = serde_json::json!({
64
+ "session_name": "team-agent-x",
65
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"OLDUUID","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
66
+ "leader_receiver": {"pane_id":"%1","owner_epoch":3,"leader_session_uuid":"OLDUUID"},
67
+ });
68
+ let event_log = crate::event_log::EventLog::new(&ws);
69
+ // %1 (the recorded owner) is NOT live; only the caller %5 is live.
70
+ let live = seeded_liveness(&["%5"]);
71
+ let r = claim_lease_no_incident(
72
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
73
+ )
74
+ .unwrap();
75
+ assert!(r.ok);
76
+ assert_eq!(r.status, LeaseStatus::Claimed);
77
+ assert_eq!(r.reason, Some(LeaseReason::PreviousOwnerPaneDead));
78
+ assert_eq!(r.owner_epoch, Some(OwnerEpoch(4)), "dead-owner recover 3→4");
79
+ assert_eq!(r.owner.as_ref().unwrap().pane_id, caller);
80
+ }
81
+
82
+ // RED — LIVE-OWNER REFUSAL (no --confirm): bound pane %1 live, caller %5,
83
+ // confirm=false → refused force_confirm_required + action + bound_pane_id=%1
84
+ // + owner_epoch=2 (precheck epoch, NOT advanced). golden /tmp/probe_confirm.py.
85
+ #[test]
86
+ #[serial_test::serial(env)]
87
+ fn claim_lease_no_incident_live_owner_refuses_without_confirm() {
88
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
89
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
90
+ let ws = p2_temp_ws("claim_live");
91
+ let team_id = TeamKey::new("current");
92
+ let caller = PaneId::new("%5");
93
+ let mut state = serde_json::json!({
94
+ "session_name": "team-agent-x",
95
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"OWNERUUID","owner_epoch":2,"claimed_at":"t","claimed_via":"claim-leader"},
96
+ "leader_receiver": {"pane_id":"%1","owner_epoch":2,"leader_session_uuid":"OWNERUUID"},
97
+ });
98
+ let event_log = crate::event_log::EventLog::new(&ws);
99
+ // both %1 (live owner, matching uuid) and %5 (eligible caller) live.
100
+ let live = seeded_liveness(&["%1", "%5"]);
101
+ let r = claim_lease_no_incident(
102
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
103
+ )
104
+ .unwrap();
105
+ assert!(!r.ok);
106
+ assert_eq!(r.status, LeaseStatus::Refused);
107
+ assert_eq!(r.reason, Some(LeaseReason::ForceConfirmRequired));
108
+ assert_eq!(
109
+ r.action.as_deref(),
110
+ Some("rerun with --confirm to take over the live leader pane")
111
+ );
112
+ assert_eq!(r.bound_pane_id, Some(PaneId::new("%1")), "refusal carries bound pane");
113
+ assert_eq!(r.owner_epoch, Some(OwnerEpoch(2)), "refused → precheck epoch, NOT advanced");
114
+ }
115
+
116
+ // RED — ALREADY-BOUND: caller pane == bound pane → status=AlreadyBound,
117
+ // ok=true, owner_epoch unchanged (no advance, no rewrite). golden probe_claimed.py.
118
+ #[test]
119
+ #[serial_test::serial(env)]
120
+ fn claim_lease_no_incident_already_bound_when_caller_is_bound_pane() {
121
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
122
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
123
+ let ws = p2_temp_ws("claim_bound");
124
+ let team_id = TeamKey::new("current");
125
+ let caller = PaneId::new("%5");
126
+ let mut state = serde_json::json!({
127
+ "session_name": "team-agent-x",
128
+ "team_owner": {"pane_id":"%5","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":1,"claimed_at":"t","claimed_via":"claim-leader"},
129
+ "leader_receiver": {"pane_id":"%5","owner_epoch":1,"leader_session_uuid":"U"},
130
+ });
131
+ let event_log = crate::event_log::EventLog::new(&ws);
132
+ let live = seeded_liveness(&["%5"]);
133
+ let r = claim_lease_no_incident(
134
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
135
+ )
136
+ .unwrap();
137
+ assert!(r.ok);
138
+ assert_eq!(r.status, LeaseStatus::AlreadyBound);
139
+ assert_eq!(r.owner_epoch, Some(OwnerEpoch(1)), "already-bound → epoch unchanged");
140
+ assert!(r.reason.is_none(), "already-bound path carries no acquire reason");
141
+ }
142
+
143
+ #[test]
144
+ #[serial_test::serial(env)]
145
+ fn claim_leader_persists_full_tmux_endpoint_when_tmux_tmpdir_differs_from_coordinator() {
146
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
147
+ let leader_socket = "/tmp/ta-leader-root/tmux-501/dl2f";
148
+ let _e = EnvGuard::apply(&[
149
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
150
+ ("TMUX", Some("/tmp/ta-leader-root/tmux-501/dl2f,12345,0")),
151
+ ("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
152
+ ]);
153
+ let ws = p2_temp_ws("claim_full_endpoint");
154
+ let team_id = TeamKey::new("current");
155
+ let caller = PaneId::new("%5");
156
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
157
+ let event_log = crate::event_log::EventLog::new(&ws);
158
+ let live = seeded_liveness(&["%5"]);
159
+
160
+ let r = claim_lease_no_incident(
161
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
162
+ )
163
+ .unwrap();
164
+
165
+ assert!(r.ok);
166
+ assert_eq!(r.status, LeaseStatus::Claimed);
167
+ assert_eq!(
168
+ r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
169
+ Some(leader_socket),
170
+ "claim-leader must persist the full $TMUX socket path, not only the -L short name"
171
+ );
172
+ let persisted: serde_json::Value = serde_json::from_str(
173
+ &std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
174
+ )
175
+ .unwrap();
176
+ assert_eq!(
177
+ persisted["leader_receiver"]["tmux_socket"],
178
+ serde_json::json!(leader_socket),
179
+ "state.json must carry enough endpoint information for later delivery to use tmux -S"
180
+ );
181
+ }
182
+
183
+ #[test]
184
+ #[serial_test::serial(env)]
185
+ fn claim_leader_already_bound_requires_same_delivery_endpoint_not_only_same_pane_id() {
186
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
187
+ let current_socket = "/tmp/ta-current-leader-root/tmux-501/dl2f";
188
+ let stale_socket = "/tmp/ta-stale-leader-root/tmux-501/dl2f";
189
+ let _e = EnvGuard::apply(&[
190
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
191
+ ("TMUX", Some("/tmp/ta-current-leader-root/tmux-501/dl2f,12345,0")),
192
+ ("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
193
+ ]);
194
+ let ws = p2_temp_ws("claim_bound_endpoint");
195
+ let team_id = TeamKey::new("current");
196
+ let caller = PaneId::new("%5");
197
+ let mut state = serde_json::json!({
198
+ "session_name": "team-agent-x",
199
+ "team_owner": {
200
+ "pane_id":"%5",
201
+ "provider":"codex",
202
+ "machine_fingerprint":"fp",
203
+ "leader_session_uuid":"U",
204
+ "owner_epoch":1,
205
+ "claimed_at":"t",
206
+ "claimed_via":"claim-leader",
207
+ "tmux_socket": stale_socket
208
+ },
209
+ "leader_receiver": {
210
+ "pane_id":"%5",
211
+ "owner_epoch":1,
212
+ "leader_session_uuid":"U",
213
+ "tmux_socket": stale_socket
214
+ },
215
+ });
216
+ let event_log = crate::event_log::EventLog::new(&ws);
217
+ let live = seeded_liveness(&["%5"]);
218
+
219
+ let r = claim_lease_no_incident(
220
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
221
+ )
222
+ .unwrap();
223
+
224
+ assert_ne!(
225
+ r.status,
226
+ LeaseStatus::AlreadyBound,
227
+ "already_bound must verify the same delivery endpoint is reachable; matching bare pane \
228
+ ids are not sufficient because different tmux socket roots can both have %5"
229
+ );
230
+ assert_eq!(
231
+ r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
232
+ Some(current_socket),
233
+ "claim should refresh the receiver to the caller's current full tmux endpoint"
234
+ );
235
+ }
236
+
237
+ #[test]
238
+ #[serial_test::serial(env)]
239
+ fn claim_leader_already_bound_normalizes_short_tmux_endpoint_to_full_path() {
240
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
241
+ let current_socket = "/tmp/ta-current-leader-root/tmux-501/dl9aa40c88";
242
+ let short_socket = "dl9aa40c88";
243
+ let _e = EnvGuard::apply(&[
244
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
245
+ ("TMUX", Some("/tmp/ta-current-leader-root/tmux-501/dl9aa40c88,12345,0")),
246
+ ("TMUX_TMPDIR", Some("/tmp/ta-coordinator-root")),
247
+ ]);
248
+ let ws = p2_temp_ws("claim_bound_short_endpoint");
249
+ let team_id = TeamKey::new("current");
250
+ let caller = PaneId::new("%5");
251
+ let mut state = serde_json::json!({
252
+ "session_name": "team-agent-x",
253
+ "team_owner": {
254
+ "pane_id":"%5",
255
+ "provider":"codex",
256
+ "machine_fingerprint":"fp",
257
+ "leader_session_uuid":"U",
258
+ "owner_epoch":1,
259
+ "claimed_at":"t",
260
+ "claimed_via":"claim-leader",
261
+ "tmux_socket": short_socket
262
+ },
263
+ "leader_receiver": {
264
+ "pane_id":"%5",
265
+ "owner_epoch":1,
266
+ "leader_session_uuid":"U",
267
+ "tmux_socket": short_socket
268
+ },
269
+ });
270
+ let event_log = crate::event_log::EventLog::new(&ws);
271
+ let live = seeded_liveness(&["%5"]);
272
+
273
+ let r = claim_lease_no_incident(
274
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
275
+ )
276
+ .unwrap();
277
+
278
+ assert_ne!(
279
+ r.status,
280
+ LeaseStatus::AlreadyBound,
281
+ "claim already_bound must not treat a stored short socket name as equivalent to the \
282
+ caller's full $TMUX endpoint by basename; it must refresh/normalize the receiver"
283
+ );
284
+ assert_eq!(
285
+ r.receiver.as_ref().and_then(|receiver| receiver.tmux_socket.as_deref()),
286
+ Some(current_socket),
287
+ "claim should rewrite any legacy short leader_receiver.tmux_socket to the full physical endpoint"
288
+ );
289
+ let persisted: serde_json::Value = serde_json::from_str(
290
+ &std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
291
+ )
292
+ .unwrap();
293
+ assert_eq!(
294
+ persisted["leader_receiver"]["tmux_socket"],
295
+ serde_json::json!(current_socket),
296
+ "state must not preserve a short endpoint after explicit claim; state={persisted}"
297
+ );
298
+ }
299
+
300
+ // RED — NOT-IN-TMUX-PANE: empty caller pane → refused not_in_tmux_pane with
301
+ // the EXACT golden action string (differs from the current claim_leader stub
302
+ // string). golden /tmp/probe_claim.py _lease_refused("not_in_tmux_pane",...).
303
+ #[test]
304
+ #[serial_test::serial(env)]
305
+ fn claim_lease_no_incident_empty_caller_refuses_not_in_tmux_pane() {
306
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
307
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
308
+ let ws = p2_temp_ws("claim_nopane");
309
+ let team_id = TeamKey::new("current");
310
+ let empty = PaneId::new("");
311
+ let mut state = serde_json::json!({});
312
+ let event_log = crate::event_log::EventLog::new(&ws);
313
+ let live = seeded_liveness(&[]);
314
+ let r = claim_lease_no_incident(
315
+ &ws, &mut state, None, &team_id, &empty, false, &event_log, &live,
316
+ )
317
+ .unwrap();
318
+ assert!(!r.ok);
319
+ assert_eq!(r.status, LeaseStatus::Refused);
320
+ assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
321
+ assert_eq!(
322
+ r.action.as_deref(),
323
+ Some("run team-agent claim-leader from the leader's tmux pane"),
324
+ "byte-parity: not_in_tmux_pane action string from _lease_refused (probe_claim.py)"
325
+ );
326
+ }
327
+
328
+ // OLD (Python parity, pre-Bug-3): caller pane present but NOT in the live
329
+ // leader-shape set → refused with CallerNotLeaderShaped + eligibility action
330
+ // ("from a leader (claude/codex) tmux pane").
331
+ // NEW (Bug 3 / I-RN-3 / tests/explicit_claim_takeover_any_live_pane_red):
332
+ // the hard leader-shape gate is removed by design. claim_lease_no_incident
333
+ // now accepts any LIVE caller pane (Codex / Claude / any worker shape). The
334
+ // only remaining caller refusal on this no-incident path is "caller pane is
335
+ // not live at all"; with seeded_liveness(&[]) the caller %7 is therefore
336
+ // refused as CallerPaneNotLive (CallerNotLeaderShaped is no longer produced
337
+ // on this code path).
338
+ #[test]
339
+ #[serial_test::serial(env)]
340
+ fn claim_lease_no_incident_caller_not_live_refused_under_n_rn3_any_live_pane() {
341
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
342
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
343
+ let ws = p2_temp_ws("claim_notleader");
344
+ let team_id = TeamKey::new("current");
345
+ let caller = PaneId::new("%7");
346
+ let mut state = serde_json::json!({});
347
+ let event_log = crate::event_log::EventLog::new(&ws);
348
+ // Empty live-set → caller %7 is not a live pane at all.
349
+ let live = seeded_liveness(&[]);
350
+ let r = claim_lease_no_incident(
351
+ &ws, &mut state, None, &team_id, &caller, false, &event_log, &live,
352
+ )
353
+ .unwrap();
354
+ assert!(!r.ok);
355
+ assert_eq!(r.status, LeaseStatus::Refused);
356
+ assert_eq!(r.reason, Some(LeaseReason::CallerPaneNotLive));
357
+ }
358
+
359
+ // ── 14b. leader_identity dict — workspace_abspath key (golden __init__.py:363) ──
360
+ //
361
+ // LOCK→RED: leader_identity IS implemented (owner_bind.rs:22) but its dict
362
+ // OMITS the golden `workspace_abspath` key (probe_lid.py shows it sits between
363
+ // machine_fingerprint and os_user). This pins the missing key so the porter
364
+ // must add it. Fails today (key absent → null) = RED against the omission.
365
+ #[test]
366
+ #[serial_test::serial(env)]
367
+ fn leader_identity_dict_includes_workspace_abspath_key() {
368
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
369
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
370
+ let ws = p2_temp_ws("lid_wsabs");
371
+ let v = leader_identity(&ws, None).unwrap();
372
+ let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
373
+ // golden key present + equals the resolved workspace abspath from context.
374
+ assert!(
375
+ v.get("workspace_abspath").is_some(),
376
+ "golden leader_identity dict carries 'workspace_abspath' (probe_lid.py)"
377
+ );
378
+ assert_eq!(
379
+ v["workspace_abspath"].as_str().unwrap(),
380
+ ctx.workspace_abspath.to_string_lossy(),
381
+ "workspace_abspath must equal the context's resolved abspath"
382
+ );
383
+ }
384
+
385
+ // LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
386
+ // [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
387
+ // current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
388
+ #[test]
389
+ #[serial_test::serial(env)]
390
+ fn leader_identity_dict_has_exact_nine_golden_keys() {
391
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
392
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
393
+ let ws = p2_temp_ws("lid_keys");
394
+ let v = leader_identity(&ws, None).unwrap();
395
+ let obj = v.as_object().expect("leader_identity → JSON object");
396
+ for key in [
397
+ "ok", "uuid_prefix", "machine_fingerprint", "workspace_abspath",
398
+ "os_user", "team_id", "current_pane_id", "last_seen_at", "source",
399
+ ] {
400
+ assert!(obj.contains_key(key), "golden leader_identity dict must carry '{key}'");
401
+ }
402
+ }
403
+
404
+ // ═════════════════════════════════════════════════════════════════════════
405
+ // WAVE-2 Lane B — leader-lease DIVERGENCE round (adversarial review @ 2cd71ce).
406
+ // The lease gate is green but NOT byte-parity. Golden: leader/__init__.py
407
+ // (_claim_lease_no_incident:598, _receiver_from_claim_target:861, new_owner:686,
408
+ // _lease_epoch:400, _emit_lease_refusal:469, audit :712-729). These REDs lock the EXACT
409
+ // golden shape per divergence; driven via claim_lease_no_incident(+seeded_liveness), OS-safe.
410
+ // ═════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,125 @@
1
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
+ use super::*;
3
+ use std::cell::Cell;
4
+
5
+ // ── helpers ────────────────────────────────────────────────────────────
6
+ /// derive 一个真 LeaderSessionUuid(骨架无公开裸构造器,仅 `derive`)。
7
+ fn uuid(fp: &str, ws: &str, user: &str, team: &str) -> LeaderSessionUuid {
8
+ LeaderSessionUuid::derive(fp, ws, user, team).unwrap()
9
+ }
10
+
11
+ /// 注入式 turn-state 分类器:MUST-NOT-13 命门 —— 统计 classify 调用次数,
12
+ /// 据此断言 idle 面经此 trait 分类、绝不直连任何 provider client。
13
+ struct CountingClassifier {
14
+ calls: Cell<usize>,
15
+ result: TurnState,
16
+ }
17
+ impl CountingClassifier {
18
+ fn new(result: TurnState) -> Self {
19
+ Self { calls: Cell::new(0), result }
20
+ }
21
+ }
22
+ impl TurnStateClassifier for CountingClassifier {
23
+ fn classify(
24
+ &self,
25
+ _provider: Provider,
26
+ session_log_text: &str,
27
+ ) -> Result<TurnClassification, LeaderError> {
28
+ self.calls.set(self.calls.get() + 1);
29
+ // 空 session-log 文本 → unknown(bug-085:None rollout_path 漏穿后读到空串)。
30
+ let state = if session_log_text.is_empty() { TurnState::Unknown } else { self.result };
31
+ Ok(TurnClassification {
32
+ state,
33
+ turn_id: None,
34
+ annotations: vec![],
35
+ reason: if state == TurnState::Unknown { Some("empty_session_log".into()) } else { None },
36
+ })
37
+ }
38
+ }
39
+
40
+ static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
41
+
42
+ /// RAII env override: restores prior values on drop (incl. panic unwind), so a
43
+ /// RED assertion-panic cannot leak a dirty env into other tests.
44
+ struct EnvGuard {
45
+ saved: Vec<(String, Option<String>)>,
46
+ }
47
+ impl EnvGuard {
48
+ fn apply(vars: &[(&str, Option<&str>)]) -> Self {
49
+ let saved = vars.iter().map(|(k, _)| ((*k).to_string(), std::env::var(k).ok())).collect();
50
+ for (k, v) in vars {
51
+ match v {
52
+ Some(val) => std::env::set_var(k, val),
53
+ None => std::env::remove_var(k),
54
+ }
55
+ }
56
+ Self { saved }
57
+ }
58
+ }
59
+ impl Drop for EnvGuard {
60
+ fn drop(&mut self) {
61
+ for (k, v) in &self.saved {
62
+ match v {
63
+ Some(val) => std::env::set_var(k, val),
64
+ None => std::env::remove_var(k),
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ fn p2_temp_ws(tag: &str) -> PathBuf {
71
+ let ws = std::env::temp_dir().join(format!("ta_rs_p2leader_{}_{}", tag, std::process::id()));
72
+ std::fs::create_dir_all(&ws).unwrap();
73
+ ws
74
+ }
75
+
76
+ // P1 — state-supplied leader_session_uuid → source 'derived' (NOT 'env'); Python's
77
+ // source is binary override-vs-derived (leader/__init__.py:206).
78
+ struct SeededLiveness {
79
+ live_panes: std::collections::BTreeSet<String>,
80
+ }
81
+ impl crate::state::owner_gate::PaneLivenessProbe for SeededLiveness {
82
+ fn liveness(&self, pane_id: &str) -> crate::model::enums::PaneLiveness {
83
+ if self.live_panes.contains(pane_id) {
84
+ crate::model::enums::PaneLiveness::Live
85
+ } else {
86
+ crate::model::enums::PaneLiveness::Dead
87
+ }
88
+ }
89
+ }
90
+
91
+ fn seeded_liveness(panes: &[&str]) -> SeededLiveness {
92
+ SeededLiveness { live_panes: panes.iter().map(|p| (*p).to_string()).collect() }
93
+ }
94
+
95
+ // RED — VACANT ACQUIRE: empty state + eligible caller → status=Claimed,
96
+ // reason=vacant_acquired, owner_epoch 0→1, owner+receiver bound to caller,
97
+ // claimed_via=claim-leader, BOTH state files written (dual-state).
98
+ // golden /tmp/probe_claimed.py: epoch 1, claimed_via "claim-leader",
99
+ // discovery "claim_leader", reason "vacant_acquired".
100
+ /// Event names from the workspace event log (events.jsonl), in write order.
101
+ fn event_names(ws: &std::path::Path) -> Vec<String> {
102
+ let path = crate::model::paths::logs_dir(ws).join("events.jsonl");
103
+ std::fs::read_to_string(&path)
104
+ .map(|t| {
105
+ t.lines()
106
+ .filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
107
+ .filter_map(|e| e.get("event").and_then(|v| v.as_str()).map(String::from))
108
+ .collect()
109
+ })
110
+ .unwrap_or_default()
111
+ }
112
+
113
+ // D1 [BLOCK] — claim-path team_owner is EXACTLY golden's 7 keys (NO os_user). Golden new_owner
114
+ // (__init__.py:686-694) = pane_id,provider,machine_fingerprint,leader_session_uuid,owner_epoch,
115
+ // claimed_at,claimed_via; only Family-A leader_binding writes os_user, NEVER the claim path. Rust
116
+ // make_owner sets os_user:Some(..) + TeamOwner has no skip_serializing_if -> 8 keys. RED.
117
+
118
+ mod basics;
119
+ mod wake_start_owner;
120
+ mod lease_api;
121
+ mod idle;
122
+ mod identity;
123
+ mod lease_claim;
124
+ mod byte_findings;
125
+ mod rediscover;