@team-agent/installer 0.5.61 → 0.5.62

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 (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +1 -0
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  49. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -0
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,951 @@
1
+ //! RS 0.3.1 dogfood BUG-2/BUG-4 window consistency contracts.
2
+ //!
3
+ //! Real-machine evidence:
4
+ //! - state/status reported `clauder` running, but tmux only listed the `codexer` window.
5
+ //! - worker->leader delivery queued, then the coordinator pasted to `team-verify031:leader`,
6
+ //! but that window did not exist; state had a bound `leader_receiver.pane_id`.
7
+ //!
8
+ //! These are command-substitute contracts: no real provider or subscription is used, but the
9
+ //! observable invariant is the same as the Mac mini run.
10
+
11
+ #![allow(clippy::expect_used, clippy::panic)]
12
+
13
+ use std::collections::BTreeMap;
14
+ use std::path::PathBuf;
15
+ use std::sync::{Arc, Mutex};
16
+
17
+ use serde_json::json;
18
+ use serial_test::{file_serial, serial};
19
+ use team_agent::event_log::EventLog;
20
+ use team_agent::lifecycle::{launch_with_transport, quick_start_with_transport, QuickStartReport};
21
+ use team_agent::message_store::MessageStore;
22
+ use team_agent::messaging::{
23
+ deliver_pending_message, deliver_pending_messages, send_message, DeliveryStatus, MessageTarget,
24
+ SendOptions, TrustedSender,
25
+ };
26
+ use team_agent::model::ids::AgentId;
27
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
28
+ use team_agent::transport::{
29
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
30
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
31
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
32
+ TurnVerification, WindowName,
33
+ };
34
+
35
+ #[path = "../../../tests/support/hermetic.rs"]
36
+ mod hermetic;
37
+ use hermetic::HermeticTestEnv;
38
+
39
+ const TEAM_MD: &str =
40
+ "---\nname: verify031\nobjective: Verify mixed provider windows.\nprovider: codex\n---\n\nTeam.\n";
41
+
42
+ const CODEX_ROLE: &str = "---\nname: codexer\nrole: Codex Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nCodex worker.\n";
43
+
44
+ const CLAUDE_ROLE: &str = "---\nname: clauder\nrole: Claude Worker\nprovider: claude\nmodel: claude-sonnet-4-6\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nClaude worker.\n";
45
+
46
+ #[test]
47
+ #[serial(env)]
48
+ fn worker_mcp_owner_team_scope_must_match_runtime_team_key_not_spec_name() {
49
+ let hermetic = HermeticTestEnv::enter("rs031-mcp-owner-scope");
50
+ let team = mixed_provider_team_dir(&hermetic, "mcp-owner-scope");
51
+ let transport = RecordingTransport::new()
52
+ .with_windows(vec![WindowName::new("clauder"), WindowName::new("codexer")]);
53
+ let launch = ready_launch(
54
+ quick_start_with_transport(&team, None, true, None, &transport)
55
+ .expect("quick-start should reach worker spawn path"),
56
+ );
57
+ assert_eq!(
58
+ launch.started.len(),
59
+ 2,
60
+ "fixture precondition: both workers spawn"
61
+ );
62
+
63
+ let workspace = team.parent().expect("teamdir has parent workspace");
64
+ let state = load_runtime_state(workspace).expect("runtime state written by launch");
65
+ let runtime_team_key = team_agent::state::projection::team_state_key(&state);
66
+ // quickstart-team-key-divergence fix: quick-start now unifies the runtime
67
+ // key on the compiled `TEAM.md name:` (`verify031`), no longer the team
68
+ // directory basename. The invariant below (worker MCP owner scope must
69
+ // equal the runtime key, whatever it is) is unchanged.
70
+ assert_eq!(
71
+ runtime_team_key, "verify031",
72
+ "fixture precondition: runtime state key is the compiled team identity"
73
+ );
74
+
75
+ let bad_scopes = transport
76
+ .spawn_envs()
77
+ .into_iter()
78
+ .filter_map(|env| env.get("TEAM_AGENT_OWNER_TEAM_ID").cloned())
79
+ .filter(|scope| scope.as_str() != runtime_team_key)
80
+ .collect::<Vec<_>>();
81
+ let bad_mcp_config_scopes = transport
82
+ .spawn_argvs()
83
+ .into_iter()
84
+ .filter(|argv| {
85
+ argv.iter().any(|arg| {
86
+ arg.contains("TEAM_AGENT_OWNER_TEAM_ID")
87
+ && !arg.contains(&format!("TEAM_AGENT_OWNER_TEAM_ID=\"{runtime_team_key}\""))
88
+ && !arg.contains(&format!("TEAM_AGENT_OWNER_TEAM_ID={runtime_team_key}"))
89
+ })
90
+ })
91
+ .collect::<Vec<_>>();
92
+ assert!(
93
+ bad_scopes.is_empty() && bad_mcp_config_scopes.is_empty(),
94
+ "BUG-4 contract: worker MCP TEAM_AGENT_OWNER_TEAM_ID must match the runtime team key used by state. \
95
+ Mac mini saw coordinator notifications under `teamdir` but worker MCP scope_resolved under the spec name, \
96
+ so send_message(to=leader) looked in the wrong team and reported leader_not_attached. \
97
+ expected_scope={runtime_team_key:?} bad_scopes={bad_scopes:?} bad_mcp_config_scopes={bad_mcp_config_scopes:?} state={state}"
98
+ );
99
+ }
100
+
101
+ #[test]
102
+ #[serial(env)]
103
+ fn quick_start_preserves_external_leader_receiver_when_worker_pane_id_collides_across_tmux_sockets()
104
+ {
105
+ let hermetic = HermeticTestEnv::enter("rs031-external-pane-collision");
106
+ let _tmux = hermetic.with_env("TMUX", "/tmp/default-tmux-socket,123,0");
107
+ let _pane = hermetic.with_env("TMUX_PANE", "%0");
108
+ let team = codex_only_team_dir(&hermetic, "external-pane-collision");
109
+ let transport = RecordingTransport::new().with_windows(vec![WindowName::new("codexer")]);
110
+ let launch = ready_launch(
111
+ quick_start_with_transport(&team, None, true, None, &transport)
112
+ .expect("quick-start should reach worker spawn path"),
113
+ );
114
+ let worker_pane = launch
115
+ .started
116
+ .iter()
117
+ .find(|agent| agent.agent_id.as_str() == "codexer")
118
+ .map(|agent| agent.target.as_str())
119
+ .expect("fixture precondition: codexer spawned");
120
+ assert_eq!(
121
+ worker_pane, "%0",
122
+ "fixture precondition: first worker in the product -L socket receives bare pane id %0"
123
+ );
124
+
125
+ let workspace = team.parent().expect("teamdir has parent workspace");
126
+ let state = load_runtime_state(workspace).expect("runtime state written by launch");
127
+ let top_receiver_pane = state
128
+ .get("leader_receiver")
129
+ .and_then(|receiver| receiver.get("pane_id"))
130
+ .and_then(serde_json::Value::as_str);
131
+ let receiver_pane = state
132
+ .pointer("/teams/verify031/leader_receiver/pane_id")
133
+ .and_then(serde_json::Value::as_str);
134
+ let top_owner_pane = state
135
+ .get("team_owner")
136
+ .and_then(|owner| owner.get("pane_id"))
137
+ .and_then(serde_json::Value::as_str);
138
+ let owner_pane = state
139
+ .pointer("/teams/verify031/team_owner/pane_id")
140
+ .and_then(serde_json::Value::as_str);
141
+
142
+ assert_eq!(
143
+ receiver_pane,
144
+ Some("%0"),
145
+ "BUG-4 socket contract: quick-start may run from a real leader pane on the user's/default tmux socket \
146
+ whose bare id is `%0`, while the first worker in the product per-team -L socket is also `%0`. \
147
+ Different sockets are not a worker/leader collision: the launched leader_receiver must remain bound \
148
+ to the caller pane instead of being cleared to unbound/leader_not_attached. \
149
+ receiver_pane={receiver_pane:?} top_receiver_pane={top_receiver_pane:?} owner_pane={owner_pane:?} \
150
+ top_owner_pane={top_owner_pane:?} \
151
+ worker_pane={worker_pane:?} state={state}"
152
+ );
153
+ assert_eq!(
154
+ owner_pane,
155
+ Some("%0"),
156
+ "BUG-4 socket contract: team_owner must not be replaced with __team_agent_unbound__ for a different-socket bare id collision; state={state}"
157
+ );
158
+ assert!(
159
+ top_receiver_pane.is_none() && top_owner_pane.is_none(),
160
+ "Stage 3d state saves strip legacy top-level owner copies; state={state}"
161
+ );
162
+ }
163
+
164
+ #[test]
165
+ #[serial(env)]
166
+ fn running_worker_state_must_only_be_written_for_windows_that_exist_after_launch() {
167
+ let hermetic = HermeticTestEnv::enter("rs031-state-window");
168
+ let team = mixed_provider_team_dir(&hermetic, "state-window");
169
+ let spec = team_agent::compiler::compile_team(&team).expect("compile mixed provider team");
170
+ let spec_path = team.join("team.spec.yaml");
171
+ std::fs::write(&spec_path, team_agent::model::yaml::dumps(&spec)).expect("write spec");
172
+
173
+ // Simulates the Mac mini final capture: the codex window exists, the claude window does not.
174
+ // launch_with_transport currently never verifies the post-spawn window set before persisting
175
+ // `status=running`, so this fixture pins the state/physical invariant directly.
176
+ let transport = RecordingTransport::new().with_windows(vec![WindowName::new("codexer")]);
177
+ let launch = launch_with_transport(&spec_path, false, true, true, &transport)
178
+ .expect("launch should reach the real spawn/state path");
179
+ assert_eq!(
180
+ launch.started.len(),
181
+ 2,
182
+ "fixture precondition: both spawn calls returned"
183
+ );
184
+
185
+ let workspace = team.parent().expect("teamdir has parent workspace");
186
+ let state = load_runtime_state(workspace).expect("runtime state written by launch");
187
+ let live_windows = transport
188
+ .list_windows(&launch.session_name)
189
+ .expect("recording transport list_windows");
190
+
191
+ let mut violations = Vec::new();
192
+ for (agent_id, agent) in state
193
+ .get("agents")
194
+ .and_then(serde_json::Value::as_object)
195
+ .expect("state agents object")
196
+ {
197
+ let status = agent.get("status").and_then(serde_json::Value::as_str);
198
+ if status != Some("running") {
199
+ continue;
200
+ }
201
+ let window = agent
202
+ .get("window")
203
+ .and_then(serde_json::Value::as_str)
204
+ .unwrap_or(agent_id);
205
+ if !live_windows.iter().any(|live| live.as_str() == window) {
206
+ violations.push(format!(
207
+ "{agent_id} status=running window={window} missing from tmux list"
208
+ ));
209
+ }
210
+ }
211
+
212
+ assert!(
213
+ violations.is_empty(),
214
+ "BUG-2 contract: state/status must not mark an agent running unless its tmux window exists. \
215
+ Mac mini saw state clauder=running while product_windows only had codexer. violations={violations:?} state={state}"
216
+ );
217
+ }
218
+
219
+ #[test]
220
+ #[serial(env)]
221
+ fn leader_bound_delivery_must_target_bound_leader_pane_not_missing_leader_window() {
222
+ let hermetic = HermeticTestEnv::enter("rs031-leader-window");
223
+ hermetic.scrub_tmux();
224
+ hermetic.assert_no_real_tmux();
225
+ assert!(
226
+ std::env::var_os("TEAM_AGENT_WORKSPACE").is_none(),
227
+ "HermeticTestEnv must scrub hostile TEAM_AGENT_WORKSPACE before BUG4 canary delivery"
228
+ );
229
+ let workspace = hermetic.workspace("leader-window");
230
+ hermetic.assert_path_under_root(&workspace);
231
+ let state = json!({
232
+ "session_name": "team-verify031",
233
+ "active_team_key": "teamdir",
234
+ "leader_receiver": {
235
+ "mode": "direct_tmux",
236
+ "status": "attached",
237
+ "provider": "codex",
238
+ "pane_id": "%0",
239
+ "owner_epoch": 1
240
+ },
241
+ "team_owner": {
242
+ "pane_id": "%0",
243
+ "provider": "codex",
244
+ "owner_epoch": 1
245
+ },
246
+ "owner_epoch": 1,
247
+ "agents": {
248
+ "codexer": {
249
+ "status": "running",
250
+ "provider": "codex",
251
+ "window": "codexer"
252
+ }
253
+ },
254
+ "teams": {
255
+ "teamdir": {
256
+ "session_name": "team-verify031",
257
+ "leader_receiver": {
258
+ "mode": "direct_tmux",
259
+ "status": "attached",
260
+ "provider": "codex",
261
+ "pane_id": "%0",
262
+ "owner_epoch": 1
263
+ },
264
+ "team_owner": {
265
+ "pane_id": "%0",
266
+ "provider": "codex",
267
+ "owner_epoch": 1
268
+ },
269
+ "agents": {
270
+ "codexer": {
271
+ "status": "running",
272
+ "provider": "codex",
273
+ "window": "codexer"
274
+ }
275
+ }
276
+ }
277
+ }
278
+ });
279
+ save_runtime_state(&workspace, &state).expect("seed runtime state");
280
+
281
+ let store = MessageStore::open(&workspace).expect("open message store");
282
+ hermetic.assert_store_under_root(&store);
283
+ let message_id = store
284
+ .create_message(
285
+ None,
286
+ "codexer",
287
+ "leader",
288
+ "BUG4 worker to leader canary",
289
+ None,
290
+ false,
291
+ Some("teamdir"),
292
+ )
293
+ .expect("create accepted leader-bound message");
294
+ let event_log = EventLog::new(&workspace);
295
+ let transport = RecordingTransport::new()
296
+ .with_windows(vec![WindowName::new("codexer")])
297
+ .with_targets(vec![pane_info("%0", "external-leader-socket", "leader")]);
298
+
299
+ let outcome = deliver_pending_message(
300
+ &workspace,
301
+ &store,
302
+ &transport,
303
+ &message_id,
304
+ &event_log,
305
+ &state,
306
+ )
307
+ .expect("delivery should use the recorded transport");
308
+ assert!(
309
+ outcome.ok,
310
+ "fixture delivery should complete through OfflineTransport"
311
+ );
312
+
313
+ let targets = transport.inject_targets();
314
+ assert_eq!(
315
+ targets.len(),
316
+ 1,
317
+ "expected exactly one physical injection target"
318
+ );
319
+ assert_eq!(
320
+ targets[0],
321
+ Target::Pane(team_agent::transport::PaneId::new("%0")),
322
+ "BUG-4 contract: leader-bound delivery must paste to state.leader_receiver.pane_id when bound. \
323
+ It must not synthesize the nonexistent session window `team-verify031:leader`; Mac mini \
324
+ failed with `can't find window: leader`. actual_targets={targets:?}"
325
+ );
326
+ }
327
+
328
+ #[test]
329
+ #[ignore = "real-machine: needs real tmux/coordinator/binary"]
330
+ #[file_serial(tmux)]
331
+ fn worker_to_leader_delivery_succeeds_when_attached_leader_pane_lives_on_default_socket() {
332
+ let hermetic = HermeticTestEnv::enter("rs031-leader-default-socket-e2e");
333
+ let workspace = hermetic.workspace("leader-default-socket-e2e");
334
+ let leader_pane = PaneId::new("%default-leader");
335
+ let product_worker_pane = PaneId::new("%product-worker");
336
+ let product_session = SessionName::new("team-bug4-product");
337
+ let transport = RecordingTransport::new()
338
+ .with_liveness(vec![
339
+ (leader_pane.as_str().to_string(), PaneLiveness::Dead),
340
+ (product_worker_pane.as_str().to_string(), PaneLiveness::Live),
341
+ ])
342
+ .with_targets(vec![
343
+ pane_info(leader_pane.as_str(), "default-leader-socket", "leader"),
344
+ pane_info(
345
+ product_worker_pane.as_str(),
346
+ product_session.as_str(),
347
+ "codexer",
348
+ ),
349
+ ]);
350
+
351
+ save_runtime_state(
352
+ &workspace,
353
+ &json!({
354
+ "session_name": product_session.as_str(),
355
+ "active_team_key": "teamdir",
356
+ "leader": {"id": "leader", "provider": "codex"},
357
+ "leader_receiver": {
358
+ "mode": "direct_tmux",
359
+ "status": "attached",
360
+ "provider": "codex",
361
+ "pane_id": leader_pane.as_str(),
362
+ "owner_epoch": 1,
363
+ "discovery": "quick_start"
364
+ },
365
+ "team_owner": {
366
+ "pane_id": leader_pane.as_str(),
367
+ "provider": "codex",
368
+ "owner_epoch": 1,
369
+ "claimed_via": "quick-start"
370
+ },
371
+ "owner_epoch": 1,
372
+ "agents": {
373
+ "codexer": {
374
+ "status": "running",
375
+ "provider": "codex",
376
+ "window": "codexer",
377
+ "pane_id": product_worker_pane.as_str(),
378
+ "startup_prompts": "handled"
379
+ }
380
+ },
381
+ "teams": {
382
+ "teamdir": {
383
+ "session_name": product_session.as_str(),
384
+ "leader_receiver": {
385
+ "mode": "direct_tmux",
386
+ "status": "attached",
387
+ "provider": "codex",
388
+ "pane_id": leader_pane.as_str(),
389
+ "owner_epoch": 1,
390
+ "discovery": "quick_start"
391
+ },
392
+ "team_owner": {
393
+ "pane_id": leader_pane.as_str(),
394
+ "provider": "codex",
395
+ "owner_epoch": 1,
396
+ "claimed_via": "quick-start"
397
+ },
398
+ "agents": {
399
+ "codexer": {
400
+ "status": "running",
401
+ "provider": "codex",
402
+ "window": "codexer",
403
+ "pane_id": product_worker_pane.as_str(),
404
+ "startup_prompts": "handled"
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }),
410
+ )
411
+ .expect("seed runtime state");
412
+
413
+ let store = MessageStore::open(&workspace).expect("open message store");
414
+ hermetic.assert_store_under_root(&store);
415
+ let message_id = store
416
+ .create_message(
417
+ None,
418
+ "codexer",
419
+ "leader",
420
+ "BUG4 worker-to-leader default socket canary",
421
+ None,
422
+ false,
423
+ Some("teamdir"),
424
+ )
425
+ .expect("seed worker-origin leader-bound message");
426
+ let state = load_runtime_state(&workspace).expect("load seeded runtime state");
427
+ let event_log = EventLog::new(&workspace);
428
+
429
+ let delivered = deliver_pending_messages(&workspace, &state, &transport, &event_log)
430
+ .expect("worker-to-leader delivery should run through the product coordinator transport");
431
+ let status = message_status(&workspace, &message_id).expect("message status exists");
432
+ let events = std::fs::read_to_string(workspace.join(".team/logs/events.jsonl"))
433
+ .expect("events.jsonl exists");
434
+ let inject_targets = transport.inject_targets();
435
+
436
+ assert!(
437
+ delivered.iter().any(|id| id == &message_id)
438
+ && status == "delivered"
439
+ && inject_targets.contains(&Target::Pane(leader_pane.clone())),
440
+ "BUG-4 E2E contract: leader_receiver is attached and combined discovery shows the leader pane live \
441
+ on the caller/default socket. A product-socket liveness probe returning Dead for that pane must not \
442
+ be interpreted as leader_not_attached; the worker->leader message must be physically injected to the \
443
+ bound leader pane. delivered={delivered:?} status={status:?} inject_targets={inject_targets:?} events={events}"
444
+ );
445
+ assert!(
446
+ !events.contains("\"leader_receiver.delivery_blocked\"")
447
+ && !events.contains("\"reason\":\"leader_not_attached\""),
448
+ "BUG-4 E2E contract: attached+live default-socket leader must not emit leader_not_attached; events={events}"
449
+ );
450
+ }
451
+
452
+ fn assert_bound_reachable_leader_send_message_injects(
453
+ binding: &str,
454
+ discovery: &str,
455
+ claimed_via: &str,
456
+ ) {
457
+ let hermetic =
458
+ HermeticTestEnv::enter(&format!("rs031-fresh-leader-single-authority-{binding}"));
459
+ let workspace = hermetic.workspace(&format!("fresh-leader-single-authority-{binding}"));
460
+ let leader_pane = PaneId::new("%claimed-leader");
461
+ let worker_pane = PaneId::new("%worker");
462
+ let state = json!({
463
+ "session_name": "team-bug4-fresh",
464
+ "active_team_key": "teamdir",
465
+ "leader": {"id": "leader", "provider": "codex"},
466
+ "leader_receiver": {
467
+ "mode": "direct_tmux",
468
+ "status": "attached",
469
+ "provider": "codex",
470
+ "pane_id": leader_pane.as_str(),
471
+ "owner_epoch": 7,
472
+ "discovery": discovery
473
+ },
474
+ "team_owner": {
475
+ "pane_id": leader_pane.as_str(),
476
+ "provider": "codex",
477
+ "owner_epoch": 7,
478
+ "claimed_via": claimed_via
479
+ },
480
+ "owner_epoch": 7,
481
+ "agents": {
482
+ "codexer": {
483
+ "status": "running",
484
+ "provider": "codex",
485
+ "window": "codexer",
486
+ "pane_id": worker_pane.as_str(),
487
+ "startup_prompts": "handled"
488
+ }
489
+ }
490
+ });
491
+ save_runtime_state(&workspace, &state).expect("seed runtime state");
492
+ let transport = RecordingTransport::new()
493
+ .with_targets(vec![
494
+ pane_info(leader_pane.as_str(), "external-leader-socket", "leader"),
495
+ pane_info(worker_pane.as_str(), "team-bug4-fresh", "codexer"),
496
+ ])
497
+ .with_liveness(vec![
498
+ (leader_pane.as_str().to_string(), PaneLiveness::Live),
499
+ (worker_pane.as_str().to_string(), PaneLiveness::Live),
500
+ ]);
501
+ let event_log = EventLog::new(&workspace);
502
+
503
+ let mut opts = SendOptions::default();
504
+ opts.sender = TrustedSender::from_runtime_identity(AgentId::new("codexer"));
505
+ opts.requires_ack = false;
506
+ opts.route_task_id = false;
507
+ opts.team = Some(team_agent::model::ids::TeamKey::new("teamdir"));
508
+ let queued = send_message(
509
+ &workspace,
510
+ &MessageTarget::Single("leader".to_string()),
511
+ &format!("BUG4 fresh post-{binding} worker-to-leader canary"),
512
+ &opts,
513
+ )
514
+ .expect("fresh leader-bound row should be created");
515
+
516
+ assert_eq!(
517
+ queued.status,
518
+ DeliveryStatus::Queued,
519
+ "BUG-4 true-entry contract: fresh messaging::send_message(to=leader) after {binding} must \
520
+ return a non-null message_id and enqueue the row. It must not be synchronously rejected \
521
+ by send.rs before the unified delivery authority runs. out={queued:?}"
522
+ );
523
+ let message_id = queued
524
+ .message_id
525
+ .as_deref()
526
+ .expect("queued row carries message id");
527
+
528
+ let delivered = deliver_pending_messages(&workspace, &state, &transport, &event_log)
529
+ .expect("deliver_pending_messages should own the physical delivery decision");
530
+ let status = message_status(&workspace, message_id).expect("message status exists");
531
+ let targets = transport.inject_targets();
532
+ let events = std::fs::read_to_string(workspace.join(".team/logs/events.jsonl"))
533
+ .expect("events.jsonl exists");
534
+
535
+ // Injection-case revision (MUST-10 boundary): the single delivery
536
+ // authority must physically inject into the claimed leader pane exactly
537
+ // once, but a transport submit is not a provider receipt — the row parks
538
+ // as injected_awaiting_receipt and must NOT be reported delivered.
539
+ assert!(
540
+ targets
541
+ .iter()
542
+ .filter(|target| **target == Target::Pane(leader_pane.clone()))
543
+ .count()
544
+ == 1,
545
+ "BUG-4 single-authority contract: exactly one physical inject into the claimed leader \
546
+ pane. targets={targets:?} events={events}"
547
+ );
548
+ assert!(
549
+ delivered.iter().all(|id| id != message_id),
550
+ "BUG-4 single-authority contract: transport submit without a provider receipt must not \
551
+ mark the row delivered. delivered={delivered:?} events={events}"
552
+ );
553
+ assert_eq!(
554
+ status, "injected_awaiting_receipt",
555
+ "BUG-4 single-authority contract: the injected row parks as \
556
+ injected_awaiting_receipt until a provider receipt. status={status:?} events={events}"
557
+ );
558
+ assert!(
559
+ !events.contains("\"reason\":\"leader_not_attached\""),
560
+ "BUG-4 single-authority contract: reachable post-{binding} fresh delivery must not emit \
561
+ leader_not_attached before the physical delivery authority runs. events={events}"
562
+ );
563
+ }
564
+
565
+ #[test]
566
+ #[serial(env)]
567
+ fn bound_reachable_leader_from_claim_or_attach_is_injected_by_single_authority() {
568
+ assert_bound_reachable_leader_send_message_injects("claim", "claim_leader", "claim-leader");
569
+ assert_bound_reachable_leader_send_message_injects("attach", "attach_leader", "attach-leader");
570
+ }
571
+
572
+ #[test]
573
+ #[serial(env)]
574
+ fn unbound_or_unreachable_worker_to_leader_remains_rebind_required_without_autodelivery() {
575
+ for (label, state, transport) in [
576
+ (
577
+ "unbound-no-leader-receiver",
578
+ json!({
579
+ "session_name": "team-bug4-unbound",
580
+ "active_team_key": "teamdir",
581
+ "leader": {"id": "leader", "provider": "codex"},
582
+ "agents": {"codexer": {"status": "running", "provider": "codex", "window": "codexer"}}
583
+ }),
584
+ RecordingTransport::new(),
585
+ ),
586
+ (
587
+ "attached-but-unreachable",
588
+ json!({
589
+ "session_name": "team-bug4-unreachable",
590
+ "active_team_key": "teamdir",
591
+ "leader": {"id": "leader", "provider": "codex"},
592
+ "leader_receiver": {
593
+ "mode": "direct_tmux",
594
+ "status": "attached",
595
+ "provider": "codex",
596
+ "pane_id": "%missing-leader",
597
+ "owner_epoch": 1
598
+ },
599
+ "team_owner": {"pane_id": "%missing-leader", "provider": "codex", "owner_epoch": 1},
600
+ "agents": {"codexer": {"status": "running", "provider": "codex", "window": "codexer"}}
601
+ }),
602
+ RecordingTransport::new()
603
+ .with_liveness(vec![("%missing-leader".to_string(), PaneLiveness::Dead)]),
604
+ ),
605
+ ] {
606
+ let hermetic = HermeticTestEnv::enter(&format!("rs031-{label}"));
607
+ hermetic.scrub_tmux();
608
+ hermetic.assert_no_real_tmux();
609
+ let workspace = hermetic.workspace(label);
610
+ save_runtime_state(&workspace, &state).expect("seed runtime state");
611
+ let event_log = EventLog::new(&workspace);
612
+ let mut opts = SendOptions::default();
613
+ opts.sender = TrustedSender::from_runtime_identity(AgentId::new("codexer"));
614
+ opts.requires_ack = false;
615
+ opts.route_task_id = false;
616
+ opts.team = Some(team_agent::model::ids::TeamKey::new("teamdir"));
617
+ let outcome = send_message(
618
+ &workspace,
619
+ &MessageTarget::Single("leader".to_string()),
620
+ "BUG4 unreachable leader canary",
621
+ &opts,
622
+ )
623
+ .expect("send_message should enqueue a leader-bound row for the delivery authority");
624
+ let message_id = outcome.message_id.as_deref().unwrap_or_else(|| {
625
+ panic!(
626
+ "{label}: send_message(to=leader) must return a non-null message_id before \
627
+ deliver_pending_messages decides rebind_required; out={outcome:?}"
628
+ )
629
+ });
630
+
631
+ assert_eq!(
632
+ outcome.status,
633
+ DeliveryStatus::Queued,
634
+ "{label}: send_message(to=leader) must not synchronously refuse before enqueue. \
635
+ It should return a non-null message_id and let deliver_pending_messages be the \
636
+ single authority for leader_not_attached/rebind_required. out={outcome:?}"
637
+ );
638
+
639
+ let delivered = deliver_pending_messages(&workspace, &state, &transport, &event_log)
640
+ .expect("single authority should reject unreachable leader rows");
641
+ assert!(
642
+ delivered.iter().all(|id| id != message_id),
643
+ "{label}: unbound/unreachable leader must not be delivered; delivered={delivered:?}"
644
+ );
645
+
646
+ let status = message_status(&workspace, message_id).expect("message status exists");
647
+ let events = std::fs::read_to_string(workspace.join(".team/logs/events.jsonl"))
648
+ .expect("events.jsonl exists");
649
+ assert_eq!(
650
+ status, "failed",
651
+ "{label}: A(a) guard: unreachable leader rows must finish blocked/failed until explicit claim/rebind; \
652
+ out={outcome:?} events={events}"
653
+ );
654
+ assert!(
655
+ events.contains("leader_not_attached") && events.contains("rebind_required"),
656
+ "{label}: A(a) guard must emit leader_not_attached + rebind_required audit; events={events}"
657
+ );
658
+ }
659
+ }
660
+
661
+ #[test]
662
+ fn send_message_entrypoint_has_no_pre_enqueue_leader_liveness_gate() {
663
+ let src = std::fs::read_to_string(concat!(
664
+ env!("CARGO_MANIFEST_DIR"),
665
+ "/src/messaging/send.rs"
666
+ ))
667
+ .expect("read send.rs");
668
+ let body = src
669
+ .split("pub fn send_message")
670
+ .nth(1)
671
+ .and_then(|tail| tail.split("fn task_exists").next())
672
+ .expect("send_message body");
673
+
674
+ for forbidden in [
675
+ "leader_pane_bound_but_not_live",
676
+ "rebind_required_outcome(None)",
677
+ "\"leader_receiver.delivery_blocked\"",
678
+ "\"leader_not_attached\"",
679
+ ] {
680
+ assert!(
681
+ !body.contains(forbidden),
682
+ "BUG-4 true-entry contract: messaging::send_message must not have a pre-enqueue \
683
+ leader liveness gate ({forbidden}); it must create a message_id and let \
684
+ deliver_pending_messages decide endpoint/liveness/injection"
685
+ );
686
+ }
687
+ }
688
+
689
+ #[test]
690
+ fn send_to_leader_receiver_entrypoint_has_no_private_attached_precheck() {
691
+ let src = std::fs::read_to_string(concat!(
692
+ env!("CARGO_MANIFEST_DIR"),
693
+ "/src/messaging/leader_receiver.rs"
694
+ ))
695
+ .expect("read leader_receiver.rs");
696
+ let body = src
697
+ .split("pub fn send_to_leader_receiver")
698
+ .nth(1)
699
+ .and_then(|tail| tail.split("pub fn mirror_peer_message_to_leader").next())
700
+ .expect("send_to_leader_receiver body");
701
+
702
+ for forbidden in [
703
+ "leader_pane_is_live",
704
+ "pane_attached",
705
+ "leader_receiver.delivery_blocked",
706
+ "leader_not_attached",
707
+ ] {
708
+ assert!(
709
+ !body.contains(forbidden),
710
+ "BUG-4 single-authority contract: send_to_leader_receiver must not carry a private \
711
+ leader-attached precheck ({forbidden}); endpoint/liveness/blocking belongs only to \
712
+ deliver_pending_messages/deliver_pending_message"
713
+ );
714
+ }
715
+ }
716
+
717
+ fn mixed_provider_team_dir(hermetic: &HermeticTestEnv, tag: &str) -> PathBuf {
718
+ let team = hermetic.workspace(tag).join("teamdir");
719
+ std::fs::create_dir_all(team.join("agents")).expect("create agents dir");
720
+ std::fs::write(team.join("TEAM.md"), TEAM_MD).expect("write TEAM.md");
721
+ std::fs::write(team.join("agents").join("clauder.md"), CLAUDE_ROLE)
722
+ .expect("write clauder role");
723
+ std::fs::write(team.join("agents").join("codexer.md"), CODEX_ROLE).expect("write codexer role");
724
+ team
725
+ }
726
+
727
+ fn message_status(workspace: &std::path::Path, message_id: &str) -> Option<String> {
728
+ let store = MessageStore::open(workspace).ok()?;
729
+ let conn = team_agent::db::schema::open_db(store.db_path()).ok()?;
730
+ conn.query_row(
731
+ "select status from messages where message_id = ?1",
732
+ [message_id],
733
+ |row| row.get(0),
734
+ )
735
+ .ok()
736
+ }
737
+
738
+ fn pane_info(pane_id: &str, session: &str, window: &str) -> PaneInfo {
739
+ PaneInfo {
740
+ pane_id: PaneId::new(pane_id),
741
+ session: SessionName::new(session),
742
+ window_index: Some(0),
743
+ window_name: Some(WindowName::new(window)),
744
+ pane_index: Some(0),
745
+ tty: None,
746
+ current_command: Some("bash".to_string()),
747
+ current_path: None,
748
+ active: true,
749
+ pane_pid: None,
750
+ leader_env: BTreeMap::new(),
751
+ }
752
+ }
753
+
754
+ #[derive(Clone, Default)]
755
+ struct RecordingTransport {
756
+ inner: Arc<Mutex<RecordingState>>,
757
+ }
758
+
759
+ #[derive(Default)]
760
+ struct RecordingState {
761
+ windows: Vec<WindowName>,
762
+ spawns: usize,
763
+ inject_targets: Vec<Target>,
764
+ spawn_envs: Vec<BTreeMap<String, String>>,
765
+ spawn_argvs: Vec<Vec<String>>,
766
+ liveness: BTreeMap<String, PaneLiveness>,
767
+ targets: Vec<PaneInfo>,
768
+ }
769
+
770
+ impl RecordingTransport {
771
+ fn new() -> Self {
772
+ Self::default()
773
+ }
774
+
775
+ fn with_windows(self, windows: Vec<WindowName>) -> Self {
776
+ self.with_state(|state| state.windows = windows);
777
+ self
778
+ }
779
+
780
+ fn with_liveness(self, entries: Vec<(String, PaneLiveness)>) -> Self {
781
+ self.with_state(|state| {
782
+ state.liveness = entries.into_iter().collect();
783
+ });
784
+ self
785
+ }
786
+
787
+ fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
788
+ self.with_state(|state| state.targets = targets);
789
+ self
790
+ }
791
+
792
+ fn inject_targets(&self) -> Vec<Target> {
793
+ self.with_state(|state| state.inject_targets.clone())
794
+ }
795
+
796
+ fn spawn_envs(&self) -> Vec<BTreeMap<String, String>> {
797
+ self.with_state(|state| state.spawn_envs.clone())
798
+ }
799
+
800
+ fn spawn_argvs(&self) -> Vec<Vec<String>> {
801
+ self.with_state(|state| state.spawn_argvs.clone())
802
+ }
803
+
804
+ fn with_state<T>(&self, f: impl FnOnce(&mut RecordingState) -> T) -> T {
805
+ let mut guard = self.inner.lock().expect("recording transport lock");
806
+ f(&mut guard)
807
+ }
808
+ }
809
+
810
+ impl Transport for RecordingTransport {
811
+ fn kind(&self) -> BackendKind {
812
+ BackendKind::Tmux
813
+ }
814
+
815
+ fn spawn_first(
816
+ &self,
817
+ session: &SessionName,
818
+ window: &WindowName,
819
+ argv: &[String],
820
+ _cwd: &std::path::Path,
821
+ env: &BTreeMap<String, String>,
822
+ ) -> Result<SpawnResult, TransportError> {
823
+ self.with_state(|state| {
824
+ state.spawn_envs.push(env.clone());
825
+ state.spawn_argvs.push(argv.to_vec());
826
+ });
827
+ Ok(self.spawn_result(session, window))
828
+ }
829
+
830
+ fn spawn_into(
831
+ &self,
832
+ session: &SessionName,
833
+ window: &WindowName,
834
+ argv: &[String],
835
+ _cwd: &std::path::Path,
836
+ env: &BTreeMap<String, String>,
837
+ ) -> Result<SpawnResult, TransportError> {
838
+ self.with_state(|state| {
839
+ state.spawn_envs.push(env.clone());
840
+ state.spawn_argvs.push(argv.to_vec());
841
+ });
842
+ Ok(self.spawn_result(session, window))
843
+ }
844
+
845
+ fn inject(
846
+ &self,
847
+ target: &Target,
848
+ _payload: &InjectPayload,
849
+ _submit: Key,
850
+ _bracketed: bool,
851
+ ) -> Result<InjectReport, TransportError> {
852
+ self.with_state(|state| state.inject_targets.push(target.clone()));
853
+ Ok(InjectReport {
854
+ stage_reached: InjectStage::Submit,
855
+ inject_verification: InjectVerification::CaptureContainsToken,
856
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
857
+ turn_verification: TurnVerification::NotYetObserved,
858
+ attempts: 1,
859
+ submit_diagnostics: None,
860
+ })
861
+ }
862
+
863
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
864
+ Ok(())
865
+ }
866
+
867
+ fn capture(
868
+ &self,
869
+ _target: &Target,
870
+ range: CaptureRange,
871
+ ) -> Result<CapturedText, TransportError> {
872
+ Ok(CapturedText {
873
+ text: "OpenAI Codex".to_string(),
874
+ range,
875
+ })
876
+ }
877
+
878
+ fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
879
+ Ok(None)
880
+ }
881
+
882
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
883
+ Ok(self
884
+ .with_state(|state| state.liveness.get(pane.as_str()).copied())
885
+ .unwrap_or(PaneLiveness::Unknown))
886
+ }
887
+
888
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
889
+ Ok(self.with_state(|state| state.targets.clone()))
890
+ }
891
+
892
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
893
+ Ok(false)
894
+ }
895
+
896
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
897
+ Ok(self.with_state(|state| state.windows.clone()))
898
+ }
899
+
900
+ fn set_session_env(
901
+ &self,
902
+ _session: &SessionName,
903
+ _key: &str,
904
+ _value: &str,
905
+ ) -> Result<SetEnvOutcome, TransportError> {
906
+ Ok(SetEnvOutcome::Applied)
907
+ }
908
+
909
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
910
+ Ok(())
911
+ }
912
+
913
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
914
+ Ok(())
915
+ }
916
+
917
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
918
+ Ok(AttachOutcome::Attached)
919
+ }
920
+ }
921
+
922
+ fn codex_only_team_dir(hermetic: &HermeticTestEnv, tag: &str) -> PathBuf {
923
+ let team = hermetic.workspace(tag).join("teamdir");
924
+ std::fs::create_dir_all(team.join("agents")).expect("create agents dir");
925
+ std::fs::write(team.join("TEAM.md"), TEAM_MD).expect("write TEAM.md");
926
+ std::fs::write(team.join("agents").join("codexer.md"), CODEX_ROLE).expect("write codex role");
927
+ team
928
+ }
929
+
930
+ fn ready_launch(report: QuickStartReport) -> team_agent::lifecycle::LaunchReport {
931
+ match report {
932
+ QuickStartReport::Ready { launch, .. } => *launch,
933
+ other => panic!("fixture expected quick-start ready report, got {other:?}"),
934
+ }
935
+ }
936
+
937
+ impl RecordingTransport {
938
+ fn spawn_result(&self, session: &SessionName, window: &WindowName) -> SpawnResult {
939
+ let pane_index = self.with_state(|state| {
940
+ let idx = state.spawns;
941
+ state.spawns = state.spawns.saturating_add(1);
942
+ idx
943
+ });
944
+ SpawnResult {
945
+ pane_id: PaneId::new(format!("%{pane_index}")),
946
+ session: session.clone(),
947
+ window: window.clone(),
948
+ child_pid: None,
949
+ }
950
+ }
951
+ }