@team-agent/installer 0.5.60 → 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 (265) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +49 -3
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +184 -0
  6. package/crates/team-agent/src/cli/emit.rs +104 -21
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +14 -2
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +296 -8
  11. package/crates/team-agent/src/cli/named_address.rs +26 -2
  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 +3 -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 +5 -1
  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/status_port/store.rs +7 -3
  25. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  27. package/crates/team-agent/src/cli/types.rs +36 -2
  28. package/crates/team-agent/src/communication_mode/mod.rs +54 -0
  29. package/crates/team-agent/src/compiler/tests.rs +5 -5
  30. package/crates/team-agent/src/compiler.rs +54 -1
  31. package/crates/team-agent/src/conpty/backend.rs +1 -0
  32. package/crates/team-agent/src/conpty/mod.rs +1 -0
  33. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  34. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  35. package/crates/team-agent/src/coordinator/health.rs +1 -0
  36. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  37. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  38. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  39. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  41. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  44. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  45. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  46. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  47. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  48. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  49. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  50. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  51. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  52. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  53. package/crates/team-agent/src/coordinator/types.rs +3 -176
  54. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  55. package/crates/team-agent/src/db/message_store.rs +47 -2
  56. package/crates/team-agent/src/db/migration.rs +5 -4
  57. package/crates/team-agent/src/db/mod.rs +1 -0
  58. package/crates/team-agent/src/db/schema.rs +1 -0
  59. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  60. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  61. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  62. package/crates/team-agent/src/fake_worker.rs +21 -1
  63. package/crates/team-agent/src/layout/manager.rs +3 -7
  64. package/crates/team-agent/src/layout/mod.rs +2 -2
  65. package/crates/team-agent/src/layout/overlay.rs +6 -1
  66. package/crates/team-agent/src/layout/placement.rs +1 -0
  67. package/crates/team-agent/src/layout/recovery.rs +3 -0
  68. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  69. package/crates/team-agent/src/layout/sessions.rs +8 -1
  70. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  71. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  72. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  73. package/crates/team-agent/src/leader/helpers.rs +1 -0
  74. package/crates/team-agent/src/leader/incident.rs +1 -0
  75. package/crates/team-agent/src/leader/lease.rs +27 -22
  76. package/crates/team-agent/src/leader/mod.rs +2 -3
  77. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  78. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  79. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  80. package/crates/team-agent/src/leader/registry.rs +3 -15
  81. package/crates/team-agent/src/leader/start.rs +1072 -186
  82. package/crates/team-agent/src/leader/takeover.rs +10 -232
  83. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  84. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  85. package/crates/team-agent/src/leader/tests/identity.rs +71 -59
  86. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  87. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  88. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  89. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  90. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  91. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  92. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  93. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  94. package/crates/team-agent/src/leader/types.rs +1 -28
  95. package/crates/team-agent/src/lib.rs +7 -0
  96. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  97. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  98. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  99. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  100. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  102. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  104. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  105. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  106. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
  107. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  108. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  109. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  110. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  111. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
  112. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  113. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  114. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  115. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  116. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  117. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  118. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  119. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  120. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  121. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  122. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  123. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  124. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  125. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  126. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  127. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  128. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  129. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  130. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  131. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  132. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  133. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  134. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  135. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  136. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  137. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  138. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  139. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  140. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  141. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  142. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  143. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  144. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  145. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  146. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  147. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  148. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  149. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  150. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  151. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  152. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  153. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  154. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  155. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  156. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  157. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  158. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  159. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  160. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  161. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  162. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  163. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  164. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  165. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  166. package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
  167. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  168. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  169. package/crates/team-agent/src/mcp_server/tools.rs +101 -66
  170. package/crates/team-agent/src/mcp_server/types.rs +6 -18
  171. package/crates/team-agent/src/mcp_server/wire.rs +32 -26
  172. package/crates/team-agent/src/messaging/activity.rs +1 -0
  173. package/crates/team-agent/src/messaging/address.rs +1 -0
  174. package/crates/team-agent/src/messaging/delivery.rs +109 -41
  175. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  176. package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
  177. package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
  178. package/crates/team-agent/src/messaging/mod.rs +10 -14
  179. package/crates/team-agent/src/messaging/peers.rs +2 -0
  180. package/crates/team-agent/src/messaging/persist.rs +3 -1
  181. package/crates/team-agent/src/messaging/presentation.rs +111 -1
  182. package/crates/team-agent/src/messaging/results.rs +166 -17
  183. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  184. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  185. package/crates/team-agent/src/messaging/send.rs +135 -89
  186. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  187. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  188. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  189. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  190. package/crates/team-agent/src/messaging/types.rs +7 -2
  191. package/crates/team-agent/src/messaging/watchers.rs +24 -13
  192. package/crates/team-agent/src/model/enums.rs +1 -0
  193. package/crates/team-agent/src/model/errors.rs +1 -0
  194. package/crates/team-agent/src/model/ids.rs +1 -0
  195. package/crates/team-agent/src/model/mod.rs +2 -0
  196. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  197. package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
  198. package/crates/team-agent/src/model/paths.rs +1 -0
  199. package/crates/team-agent/src/model/permissions.rs +3 -2
  200. package/crates/team-agent/src/model/routing.rs +1 -0
  201. package/crates/team-agent/src/model/spec.rs +17 -0
  202. package/crates/team-agent/src/model/task_graph.rs +1 -0
  203. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  204. package/crates/team-agent/src/model/yaml.rs +1 -0
  205. package/crates/team-agent/src/packaging/install.rs +9 -147
  206. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  207. package/crates/team-agent/src/packaging/mod.rs +1 -0
  208. package/crates/team-agent/src/packaging/repair.rs +2 -0
  209. package/crates/team-agent/src/packaging/tests.rs +29 -218
  210. package/crates/team-agent/src/packaging/types.rs +8 -15
  211. package/crates/team-agent/src/platform/argv.rs +4 -0
  212. package/crates/team-agent/src/platform/errors.rs +10 -1
  213. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  214. package/crates/team-agent/src/platform/mod.rs +0 -61
  215. package/crates/team-agent/src/platform/process.rs +1 -0
  216. package/crates/team-agent/src/provider/adapter.rs +1 -0
  217. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  218. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  219. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  220. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  221. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  222. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  223. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  224. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  225. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  226. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  227. package/crates/team-agent/src/provider/classify.rs +1 -0
  228. package/crates/team-agent/src/provider/faults.rs +1 -26
  229. package/crates/team-agent/src/provider/helpers.rs +1 -0
  230. package/crates/team-agent/src/provider/mod.rs +1 -1
  231. package/crates/team-agent/src/provider/session/capture.rs +41 -22
  232. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  233. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  234. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  235. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  236. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  237. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  238. package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
  239. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  240. package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
  241. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  242. package/crates/team-agent/src/provider/session_scan.rs +6 -42
  243. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  244. package/crates/team-agent/src/provider/types.rs +1 -0
  245. package/crates/team-agent/src/provider/wire.rs +1 -0
  246. package/crates/team-agent/src/state/identity.rs +16 -5
  247. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  248. package/crates/team-agent/src/state/mod.rs +2 -0
  249. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  250. package/crates/team-agent/src/state/ownership.rs +10 -4
  251. package/crates/team-agent/src/state/paths.rs +12 -0
  252. package/crates/team-agent/src/state/persist.rs +9 -3
  253. package/crates/team-agent/src/state/projection.rs +15 -2
  254. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  255. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  256. package/crates/team-agent/src/state/repository.rs +7 -0
  257. package/crates/team-agent/src/state/selector.rs +1 -0
  258. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  259. package/crates/team-agent/src/tmux_backend.rs +63 -0
  260. package/crates/team-agent/src/topology.rs +3 -0
  261. package/crates/team-agent/src/transport.rs +15 -0
  262. package/package.json +4 -4
  263. package/skills/team-agent/command-coverage.json +379 -0
  264. package/crates/team-agent/src/leader/inject.rs +0 -33
  265. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -1,14 +1,21 @@
1
- //! leader::start — leader_start_plan / start_leader / leader_session_name(派生 tmux session 名)。
1
+ //!
2
+ //! leader::start — leader_start_plan / leader_session_name(派生 tmux session 名)。
2
3
 
3
4
  use std::collections::BTreeMap;
4
5
  use std::io::{IsTerminal, Read, Write};
5
6
  use std::path::{Path, PathBuf};
6
7
  use std::process::{Command, Stdio};
7
8
 
9
+ #[cfg(target_os = "linux")]
10
+ use std::os::fd::AsRawFd;
11
+ #[cfg(unix)]
12
+ use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};
13
+
14
+ use crate::model::pane_authority_refusal as pane_refusal;
8
15
  use crate::provider::{get_adapter, Provider};
9
16
  use crate::tmux_backend::TmuxBackend;
10
17
  use crate::transport::{
11
- PaneId, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
18
+ PaneId, PaneInfo, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
12
19
  };
13
20
 
14
21
  use super::helpers::{
@@ -20,11 +27,55 @@ use super::{
20
27
  LeaderStartMode, LeaderStartPlan,
21
28
  };
22
29
 
23
- // ── leader::start — leader_start_plan / start_leader / session 名 ──
30
+ // ── leader::start — leader_start_plan / session 名 ──
31
+
32
+ pub(crate) struct PreparedLeaderStart {
33
+ plan: LeaderStartPlan,
34
+ ambient_authority: Option<VerifiedAmbientPaneAuthority>,
35
+ }
36
+
37
+ #[derive(Clone, Copy)]
38
+ enum ManagedClientAttachMode {
39
+ AttachSession,
40
+ SwitchClient,
41
+ }
42
+
43
+ const DIFFERENT_TMUX_SERVER_PREFIX: &str =
44
+ "managed launcher refuses a different ambient tmux server";
45
+
46
+ impl PreparedLeaderStart {
47
+ pub(crate) fn plan(&self) -> &LeaderStartPlan {
48
+ &self.plan
49
+ }
50
+ }
51
+
52
+ pub(crate) enum PrepareLeaderStartError {
53
+ PaneAuthorityRefused(pane_refusal::PaneAuthorityRefusal),
54
+ Leader(LeaderError),
55
+ }
56
+
57
+ impl PrepareLeaderStartError {
58
+ fn into_leader_error(self) -> LeaderError {
59
+ match self {
60
+ Self::PaneAuthorityRefused(refusal) => LeaderError::Validation(format!(
61
+ "{}; open a new terminal outside the current tmux/pane or run \
62
+ `team-agent attach-leader` from the intended workspace pane",
63
+ refusal.reason().as_str()
64
+ )),
65
+ Self::Leader(error) => error,
66
+ }
67
+ }
68
+ }
69
+
70
+ impl From<LeaderError> for PrepareLeaderStartError {
71
+ fn from(error: LeaderError) -> Self {
72
+ Self::Leader(error)
73
+ }
74
+ }
24
75
 
25
76
  /// `leader_start_plan`(card §46;`__init__.py:82`)。计算 leader 启动计划
26
77
  /// (exec in-TMUX / new tmux session / attach existing)。provider 未安装 → `Err(Start)`。
27
- pub fn leader_start_plan(
78
+ pub(crate) fn leader_start_plan(
28
79
  provider: Provider,
29
80
  provider_args: &[String],
30
81
  workspace: &Path,
@@ -32,6 +83,122 @@ pub fn leader_start_plan(
32
83
  confirm_attach: bool,
33
84
  attach_session: Option<&SessionName>,
34
85
  external_leader: bool,
86
+ ) -> Result<LeaderStartPlan, LeaderError> {
87
+ prepare_leader_start(
88
+ provider,
89
+ provider_args,
90
+ workspace,
91
+ attach_existing,
92
+ confirm_attach,
93
+ attach_session,
94
+ external_leader,
95
+ )
96
+ .map(|prepared| prepared.plan)
97
+ .map_err(PrepareLeaderStartError::into_leader_error)
98
+ }
99
+
100
+ pub(crate) fn prepare_leader_start(
101
+ provider: Provider,
102
+ provider_args: &[String],
103
+ workspace: &Path,
104
+ attach_existing: bool,
105
+ confirm_attach: bool,
106
+ attach_session: Option<&SessionName>,
107
+ external_leader: bool,
108
+ ) -> Result<PreparedLeaderStart, PrepareLeaderStartError> {
109
+ prepare_leader_start_with_nested_attach(
110
+ provider,
111
+ provider_args,
112
+ workspace,
113
+ attach_existing,
114
+ confirm_attach,
115
+ attach_session,
116
+ external_leader,
117
+ false,
118
+ )
119
+ }
120
+
121
+ pub(crate) fn prepare_leader_start_with_nested_attach(
122
+ provider: Provider,
123
+ provider_args: &[String],
124
+ workspace: &Path,
125
+ attach_existing: bool,
126
+ confirm_attach: bool,
127
+ attach_session: Option<&SessionName>,
128
+ external_leader: bool,
129
+ allow_nested_attach: bool,
130
+ ) -> Result<PreparedLeaderStart, PrepareLeaderStartError> {
131
+ let explicit_external_path = external_leader || attach_existing || attach_session.is_some();
132
+ let state_external_path = workspace_state_uses_external_leader(workspace);
133
+ let (ambient_authority, managed_client_attach_mode, managed_provider_reentry) =
134
+ if explicit_external_path || state_external_path {
135
+ (ambient_pane_authority_preflight(workspace)?, None, false)
136
+ } else if workspace_state_is_managed_provider_reentry(workspace) {
137
+ (ambient_pane_authority_preflight(workspace)?, None, true)
138
+ } else {
139
+ let (authority, attach_mode) =
140
+ managed_launcher_ambient_route(workspace, allow_nested_attach)?;
141
+ (authority, attach_mode, false)
142
+ };
143
+ let plan = leader_start_plan_with_ambient_authority(
144
+ provider,
145
+ provider_args,
146
+ workspace,
147
+ attach_existing,
148
+ confirm_attach,
149
+ attach_session,
150
+ external_leader,
151
+ std::env::var_os("TMUX").is_some(),
152
+ managed_client_attach_mode,
153
+ managed_provider_reentry,
154
+ )?;
155
+ Ok(PreparedLeaderStart {
156
+ plan,
157
+ ambient_authority,
158
+ })
159
+ }
160
+
161
+ pub(crate) fn leader_start_plan_after_ambient_authority_check(
162
+ provider: Provider,
163
+ provider_args: &[String],
164
+ workspace: &Path,
165
+ attach_existing: bool,
166
+ confirm_attach: bool,
167
+ attach_session: Option<&SessionName>,
168
+ external_leader: bool,
169
+ ) -> Result<LeaderStartPlan, LeaderError> {
170
+ let in_tmux = std::env::var_os("TMUX").is_some();
171
+ let explicit_external_path = external_leader || attach_existing || attach_session.is_some();
172
+ let managed_client_attach_mode = (!explicit_external_path).then_some(if in_tmux {
173
+ ManagedClientAttachMode::SwitchClient
174
+ } else {
175
+ ManagedClientAttachMode::AttachSession
176
+ });
177
+ leader_start_plan_with_ambient_authority(
178
+ provider,
179
+ provider_args,
180
+ workspace,
181
+ attach_existing,
182
+ confirm_attach,
183
+ attach_session,
184
+ external_leader,
185
+ in_tmux,
186
+ managed_client_attach_mode,
187
+ false,
188
+ )
189
+ }
190
+
191
+ fn leader_start_plan_with_ambient_authority(
192
+ provider: Provider,
193
+ provider_args: &[String],
194
+ workspace: &Path,
195
+ attach_existing: bool,
196
+ confirm_attach: bool,
197
+ attach_session: Option<&SessionName>,
198
+ external_leader: bool,
199
+ in_tmux: bool,
200
+ managed_client_attach_mode: Option<ManagedClientAttachMode>,
201
+ managed_provider_reentry: bool,
35
202
  ) -> Result<LeaderStartPlan, LeaderError> {
36
203
  if attach_session.is_some() && !confirm_attach {
37
204
  return Err(LeaderError::Start(
@@ -54,29 +221,29 @@ pub fn leader_start_plan(
54
221
  }
55
222
  let state = crate::state::persist::load_runtime_state(workspace).ok();
56
223
  let identity = leader_identity_context(workspace, None, state.as_ref())?;
57
- let external_path = external_leader || attach_existing || attach_session.is_some();
224
+ let external_path = external_leader
225
+ || attach_existing
226
+ || attach_session.is_some()
227
+ || state
228
+ .as_ref()
229
+ .is_some_and(crate::state::projection::state_is_external_leader);
58
230
  // 0.3.28 Step 2: managed mode now uses the SAME dedicated leader session
59
231
  // as the external path (`team-agent-leader-<provider>-<folder>-<sha1[:8]>`)
60
232
  // — Python parity. Pre-0.3.28 the managed branch used
61
233
  // `managed_team_session_name(identity) = team-<team_id>` which is the
62
234
  // worker session — that co-location is the structural root of
63
235
  // E49/E51/E53/E57-3/E60.
64
- // 0.4.10+ mirror-session fix v2 (option B): managed-mode session name
65
- // gets a per-launch nonce so each `team-agent <provider>` entry in the
66
- // same workspace creates its OWN tmux session matching the user
67
- // expectation that `tmux new-session + claude` is independent every
68
- // time. Pre-fix the managed path used the workspace-keyed name (same
69
- // session for every entry) and silently attached the second client →
70
- // UI mirror. The external/attach paths keep the stable workspace-keyed
71
- // name (per-launch nonce would break `--attach-session` reattach).
236
+ // A-22: managed mode reuses a live provider leader session by prefix;
237
+ // when no candidate exists, it falls back to a per-launch nonce. The
238
+ // external/attach paths keep the stable workspace-keyed name because a
239
+ // nonce would break `--attach-session` reattach.
72
240
  let session_name = if external_path {
73
241
  attach_session
74
242
  .cloned()
75
243
  .or_else(|| Some(leader_session_name(provider, workspace)))
76
244
  } else {
77
- Some(managed_leader_session_name(provider, workspace))
245
+ Some(managed_leader_session_for_launch(provider, workspace))
78
246
  };
79
- let in_tmux = std::env::var_os("TMUX").is_some();
80
247
  if !in_tmux {
81
248
  ensure_tmux_installed()?;
82
249
  }
@@ -89,7 +256,7 @@ pub fn leader_start_plan(
89
256
  } else {
90
257
  false
91
258
  };
92
- let mode = if !external_path && in_tmux {
259
+ let mode = if managed_provider_reentry {
93
260
  LeaderStartMode::ExecProvider
94
261
  } else if !external_path {
95
262
  LeaderStartMode::ManagedTmuxClient
@@ -108,6 +275,7 @@ pub fn leader_start_plan(
108
275
  workspace,
109
276
  session_name.as_ref(),
110
277
  &leader_env,
278
+ managed_client_attach_mode,
111
279
  )?;
112
280
  let plan_session_name = if mode == LeaderStartMode::ExecProvider && !external_path {
113
281
  None
@@ -177,44 +345,21 @@ pub(crate) fn leader_env_for_identity(
177
345
  leader_env
178
346
  }
179
347
 
180
- /// `start_leader`(card §46;`__init__.py:60`)。计算并执行 leader 启动计划(spawn + 信号处理)。
181
- /// 进程退出后 `Err`/退出码经 caller 处理(此处返 `Result` 替代 Python 的 `SystemExit`)。
182
- pub fn start_leader(
183
- provider: Provider,
184
- provider_args: &[String],
348
+ pub(crate) fn execute_prepared_leader_start(
349
+ prepared: &PreparedLeaderStart,
185
350
  workspace: &Path,
186
- attach_existing: bool,
187
- confirm_attach: bool,
188
- attach_session: Option<&SessionName>,
189
- external_leader: bool,
190
- ) -> Result<(), LeaderError> {
191
- let plan = leader_start_plan(
192
- provider,
193
- provider_args,
351
+ ) -> Result<LeaderLaunchOutcome, LeaderError> {
352
+ execute_leader_plan_after_ambient_authority(
353
+ &prepared.plan,
194
354
  workspace,
195
- attach_existing,
196
- confirm_attach,
197
- attach_session,
198
- external_leader,
199
- )?;
200
- crate::event_log::EventLog::new(workspace).write(
201
- super::LeaderEvent::LeaderStart.name(),
202
- serde_json::json!({
203
- "provider": super::helpers::provider_wire(plan.provider),
204
- "mode": serde_json::to_value(plan.mode)?,
205
- "session_name": plan.session_name.as_ref().map(|s| s.as_str().to_string()),
206
- }),
207
- )?;
208
- execute_leader_plan(&plan, workspace).map(|_| ())
355
+ prepared.ambient_authority.as_ref(),
356
+ )
209
357
  }
210
358
 
211
- /// Execute a precomputed leader launch plan.
212
- ///
213
- /// S0 exposes the seam and return model only. Lane 2 owns the real provider/tmux
214
- /// execution and workspace-socket enforcement.
215
- pub fn execute_leader_plan(
359
+ fn execute_leader_plan_after_ambient_authority(
216
360
  plan: &LeaderStartPlan,
217
361
  workspace: &Path,
362
+ ambient_authority: Option<&VerifiedAmbientPaneAuthority>,
218
363
  ) -> Result<LeaderLaunchOutcome, LeaderError> {
219
364
  if plan.mode == LeaderStartMode::ManagedTmuxClient {
220
365
  return execute_managed_leader_plan(plan, workspace);
@@ -223,21 +368,31 @@ pub fn execute_leader_plan(
223
368
  let detached = plan.mode == LeaderStartMode::NewTmuxSession
224
369
  && !std::io::stdin().is_terminal()
225
370
  && insert_detach_flag(&mut argv);
226
- if plan.mode == LeaderStartMode::ExecProvider && !plan.is_external_leader {
371
+ let bind_verified_ambient = plan.mode == LeaderStartMode::ExecProvider
372
+ && (!plan.is_external_leader || workspace_state_uses_external_leader(workspace));
373
+ if bind_verified_ambient {
374
+ let authority = ambient_authority.ok_or_else(|| {
375
+ LeaderError::Validation("exec provider ambient pane authority missing".to_string())
376
+ })?;
227
377
  // 0.5.35 (`.team/artifacts/managed-leader-provider-reentry-locate.md`
228
378
  // §5/§6): three-way classify BEFORE persist. Same physical pane =
229
379
  // provider process replacement (refresh only). Different pane with
230
380
  // an existing canonical binding = no canonical rewrite (claim /
231
381
  // takeover is the recovery path, §7). Unbound = first-time write.
232
- match classify_exec_provider_binding(workspace, plan)? {
382
+ match classify_exec_provider_binding(workspace, plan, authority)? {
233
383
  ExecProviderBinding::ManagedReentry => {
234
- refresh_managed_leader_provider_binding(plan, workspace)?;
384
+ refresh_managed_leader_provider_binding(plan, workspace, authority)?;
235
385
  }
236
386
  ExecProviderBinding::DifferentPaneAlreadyBound => {
237
387
  // No canonical state write. Provider still runs below.
238
388
  }
239
389
  ExecProviderBinding::Unbound => {
240
- persist_exec_provider_leader_binding(plan, workspace)?;
390
+ persist_exec_provider_leader_binding(
391
+ plan,
392
+ workspace,
393
+ authority,
394
+ plan.is_external_leader,
395
+ )?;
241
396
  }
242
397
  }
243
398
  } else if plan.is_external_leader {
@@ -266,6 +421,402 @@ pub fn execute_leader_plan(
266
421
  }
267
422
  }
268
423
 
424
+ struct VerifiedAmbientPaneAuthority {
425
+ pane_id: PaneId,
426
+ observed: PaneInfo,
427
+ endpoint: String,
428
+ tmux: String,
429
+ }
430
+
431
+ fn workspace_state_uses_external_leader(workspace: &Path) -> bool {
432
+ crate::state::persist::load_runtime_state_without_migrations(workspace)
433
+ .ok()
434
+ .as_ref()
435
+ .is_some_and(crate::state::projection::state_is_external_leader)
436
+ }
437
+
438
+ fn workspace_state_is_managed_provider_reentry(workspace: &Path) -> bool {
439
+ let Ok(state) = crate::state::persist::load_runtime_state_without_migrations(workspace) else {
440
+ return false;
441
+ };
442
+ if crate::state::projection::state_is_external_leader(&state) {
443
+ return false;
444
+ }
445
+ let Some(team_key) = state
446
+ .get("active_team_key")
447
+ .and_then(serde_json::Value::as_str)
448
+ else {
449
+ return false;
450
+ };
451
+ let Some(receiver) = state
452
+ .get("teams")
453
+ .and_then(serde_json::Value::as_object)
454
+ .and_then(|teams| teams.get(team_key))
455
+ .and_then(|team| team.get("leader_receiver"))
456
+ else {
457
+ return false;
458
+ };
459
+ let Some(pane) = std::env::var_os("TMUX_PANE") else {
460
+ return false;
461
+ };
462
+ let Some(tmux) = std::env::var_os("TMUX") else {
463
+ return false;
464
+ };
465
+ let tmux = tmux.to_string_lossy();
466
+ let endpoint = tmux.split(',').next().unwrap_or("").trim();
467
+ receiver.get("status").and_then(serde_json::Value::as_str) == Some("attached")
468
+ && receiver.get("pane_id").and_then(serde_json::Value::as_str) == pane.to_str()
469
+ && receiver
470
+ .get("tmux_socket")
471
+ .and_then(serde_json::Value::as_str)
472
+ == Some(endpoint)
473
+ && receiver
474
+ .get("session_name")
475
+ .and_then(serde_json::Value::as_str)
476
+ .is_some_and(|session| session.starts_with(LEADER_SESSION_PREFIX))
477
+ }
478
+
479
+ fn ambient_pane_authority_preflight(
480
+ workspace: &Path,
481
+ ) -> Result<Option<VerifiedAmbientPaneAuthority>, PrepareLeaderStartError> {
482
+ let Some(tmux) = std::env::var_os("TMUX") else {
483
+ return Ok(None);
484
+ };
485
+ let tmux = tmux.into_string().map_err(|_| {
486
+ PrepareLeaderStartError::PaneAuthorityRefused(ambient_tmux_endpoint_refusal(
487
+ workspace,
488
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::TmuxValueNotUnicode,
489
+ ))
490
+ })?;
491
+ verified_ambient_pane_authority(workspace, &tmux)
492
+ .map(Some)
493
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)
494
+ }
495
+
496
+ fn managed_launcher_ambient_route(
497
+ workspace: &Path,
498
+ allow_nested_attach: bool,
499
+ ) -> Result<
500
+ (
501
+ Option<VerifiedAmbientPaneAuthority>,
502
+ Option<ManagedClientAttachMode>,
503
+ ),
504
+ PrepareLeaderStartError,
505
+ > {
506
+ let Some(tmux) = std::env::var_os("TMUX") else {
507
+ return Ok((None, Some(ManagedClientAttachMode::AttachSession)));
508
+ };
509
+ let tmux = tmux.into_string().map_err(|_| {
510
+ PrepareLeaderStartError::PaneAuthorityRefused(ambient_tmux_endpoint_refusal(
511
+ workspace,
512
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::TmuxValueNotUnicode,
513
+ ))
514
+ })?;
515
+ let observed_endpoint = validated_ambient_tmux_endpoint(workspace, &tmux)
516
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
517
+ let target_socket_name = TmuxBackend::for_workspace(workspace)
518
+ .tmux_endpoint()
519
+ .ok_or_else(|| LeaderError::Start("workspace tmux endpoint missing".to_string()))?;
520
+ let target_endpoint = crate::tmux_backend::socket_path_for_workspace(workspace);
521
+ let same_server = target_endpoint.as_ref().is_some_and(|target| {
522
+ let observed = Path::new(&observed_endpoint)
523
+ .canonicalize()
524
+ .unwrap_or_else(|_| PathBuf::from(&observed_endpoint));
525
+ let target = target.canonicalize().unwrap_or_else(|_| target.clone());
526
+ observed == target
527
+ });
528
+ if same_server {
529
+ let authority = verified_ambient_pane_authority(workspace, &tmux)
530
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
531
+ return Ok((Some(authority), Some(ManagedClientAttachMode::SwitchClient)));
532
+ }
533
+ if !allow_nested_attach {
534
+ return Err(LeaderError::Validation(format!(
535
+ "{DIFFERENT_TMUX_SERVER_PREFIX}: observed_endpoint={observed_endpoint}; \
536
+ requested_workspace_socket={target_socket_name}"
537
+ ))
538
+ .into());
539
+ }
540
+ let authority = verified_ambient_pane_authority(workspace, &tmux)
541
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
542
+ Ok((
543
+ Some(authority),
544
+ Some(ManagedClientAttachMode::AttachSession),
545
+ ))
546
+ }
547
+
548
+ fn validated_ambient_tmux_endpoint(
549
+ workspace: &Path,
550
+ tmux: &str,
551
+ ) -> Result<String, pane_refusal::PaneAuthorityRefusal> {
552
+ let tuple = tmux.split(',').map(str::trim).collect::<Vec<_>>();
553
+ let [endpoint, server_pid, session_index] = tuple.as_slice() else {
554
+ return Err(ambient_tmux_endpoint_refusal(
555
+ workspace,
556
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::TmuxTupleFieldCountInvalid,
557
+ ));
558
+ };
559
+ if endpoint.is_empty() {
560
+ return Err(ambient_tmux_endpoint_refusal(
561
+ workspace,
562
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::EndpointMissing,
563
+ ));
564
+ }
565
+ if !Path::new(endpoint).is_absolute() {
566
+ return Err(ambient_tmux_endpoint_refusal(
567
+ workspace,
568
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::EndpointNotAbsolute,
569
+ ));
570
+ }
571
+ if server_pid.parse::<u32>().is_err() {
572
+ return Err(ambient_tmux_endpoint_refusal(
573
+ workspace,
574
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::ServerPidInvalid,
575
+ ));
576
+ }
577
+ if session_index.parse::<u32>().is_err() {
578
+ return Err(ambient_tmux_endpoint_refusal(
579
+ workspace,
580
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::SessionIndexInvalid,
581
+ ));
582
+ }
583
+ Ok((*endpoint).to_string())
584
+ }
585
+
586
+ fn verified_ambient_pane_authority(
587
+ workspace: &Path,
588
+ tmux: &str,
589
+ ) -> Result<VerifiedAmbientPaneAuthority, pane_refusal::PaneAuthorityRefusal> {
590
+ let requested_workspace = resolve_workspace_for_hash(workspace);
591
+ let endpoint = validated_ambient_tmux_endpoint(workspace, tmux)?;
592
+ let pane_id = match std::env::var_os("TMUX_PANE") {
593
+ None => {
594
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
595
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(
596
+ pane_refusal::AmbientPaneIdUnavailableFacts {
597
+ requested_workspace,
598
+ observed_pane_id: pane_refusal::UnavailablePaneAuthorityFact::new(
599
+ pane_refusal::AmbientPaneIdUnavailableCause::EnvironmentValueMissing,
600
+ ),
601
+ endpoint,
602
+ },
603
+ ),
604
+ ));
605
+ }
606
+ Some(value) => match value.into_string() {
607
+ Ok(value) if value.trim().is_empty() => {
608
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
609
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(
610
+ pane_refusal::AmbientPaneIdUnavailableFacts {
611
+ requested_workspace,
612
+ observed_pane_id: pane_refusal::UnavailablePaneAuthorityFact::new(
613
+ pane_refusal::AmbientPaneIdUnavailableCause::EnvironmentValueEmpty,
614
+ ),
615
+ endpoint,
616
+ },
617
+ ),
618
+ ));
619
+ }
620
+ Ok(value) => PaneId::new(value.trim().to_string()),
621
+ Err(_) => {
622
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
623
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(
624
+ pane_refusal::AmbientPaneIdUnavailableFacts {
625
+ requested_workspace,
626
+ observed_pane_id: pane_refusal::UnavailablePaneAuthorityFact::new(
627
+ pane_refusal::AmbientPaneIdUnavailableCause::EnvironmentValueNotUnicode,
628
+ ),
629
+ endpoint,
630
+ },
631
+ ),
632
+ ));
633
+ }
634
+ },
635
+ };
636
+ let observed = current_tmux_pane_info(&pane_id, &endpoint).map_err(|error| {
637
+ let cause = match error {
638
+ CurrentTmuxPaneInfoError::QueryFailed => {
639
+ pane_refusal::AmbientPaneWorkspaceUnavailableCause::PaneQueryFailed
640
+ }
641
+ CurrentTmuxPaneInfoError::PaneNotFound => {
642
+ pane_refusal::AmbientPaneWorkspaceUnavailableCause::PaneNotFound
643
+ }
644
+ };
645
+ pane_refusal::PaneAuthorityRefusal::new(
646
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneWorkspaceUnavailable(
647
+ pane_refusal::AmbientPaneWorkspaceUnavailableFacts {
648
+ requested_workspace: requested_workspace.clone(),
649
+ observed_pane_id: pane_id.as_str().to_string(),
650
+ observed_pane_workspace: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
651
+ endpoint: endpoint.clone(),
652
+ },
653
+ ),
654
+ )
655
+ })?;
656
+ let pane_workspace = observed.current_path.clone().ok_or_else(|| {
657
+ pane_refusal::PaneAuthorityRefusal::new(
658
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneWorkspaceUnavailable(
659
+ pane_refusal::AmbientPaneWorkspaceUnavailableFacts {
660
+ requested_workspace: requested_workspace.clone(),
661
+ observed_pane_id: pane_id.as_str().to_string(),
662
+ observed_pane_workspace: pane_refusal::UnavailablePaneAuthorityFact::new(
663
+ pane_refusal::AmbientPaneWorkspaceUnavailableCause::CurrentPathMissing,
664
+ ),
665
+ endpoint: endpoint.clone(),
666
+ },
667
+ ),
668
+ )
669
+ })?;
670
+ let caller_tty = caller_controlling_tty().map_err(|cause| {
671
+ pane_refusal::PaneAuthorityRefusal::new(
672
+ pane_refusal::PaneAuthorityRefusalFacts::CallerControllingTtyUnavailable(
673
+ pane_refusal::CallerControllingTtyUnavailableFacts {
674
+ requested_workspace: requested_workspace.clone(),
675
+ observed_pane_id: pane_id.as_str().to_string(),
676
+ endpoint: endpoint.clone(),
677
+ caller_controlling_tty: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
678
+ },
679
+ ),
680
+ )
681
+ })?;
682
+ let pane_tty_path = observed.tty.as_deref().ok_or_else(|| {
683
+ pane_refusal::PaneAuthorityRefusal::new(
684
+ pane_refusal::PaneAuthorityRefusalFacts::ObservedPaneTtyUnavailable(
685
+ pane_refusal::ObservedPaneTtyUnavailableFacts {
686
+ requested_workspace: requested_workspace.clone(),
687
+ observed_pane_id: pane_id.as_str().to_string(),
688
+ endpoint: endpoint.clone(),
689
+ observed_pane_tty: pane_refusal::UnavailablePaneAuthorityFact::new(
690
+ pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyMissing,
691
+ ),
692
+ },
693
+ ),
694
+ )
695
+ })?;
696
+ let pane_tty = pane_tty_device(pane_tty_path).map_err(|cause| {
697
+ pane_refusal::PaneAuthorityRefusal::new(
698
+ pane_refusal::PaneAuthorityRefusalFacts::ObservedPaneTtyUnavailable(
699
+ pane_refusal::ObservedPaneTtyUnavailableFacts {
700
+ requested_workspace: requested_workspace.clone(),
701
+ observed_pane_id: pane_id.as_str().to_string(),
702
+ endpoint: endpoint.clone(),
703
+ observed_pane_tty: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
704
+ },
705
+ ),
706
+ )
707
+ })?;
708
+ if caller_tty != pane_tty {
709
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
710
+ pane_refusal::PaneAuthorityRefusalFacts::PaneTtyMismatch(
711
+ pane_refusal::PaneTtyMismatchFacts {
712
+ requested_workspace,
713
+ observed_pane_id: pane_id.as_str().to_string(),
714
+ endpoint,
715
+ caller_controlling_tty: caller_tty,
716
+ observed_pane_tty: pane_tty,
717
+ },
718
+ ),
719
+ ));
720
+ }
721
+ if !crate::messaging::leader_channel::path_is_in_workspace(&pane_workspace, workspace) {
722
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
723
+ pane_refusal::PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(
724
+ pane_refusal::PaneWorkspaceMismatchFacts {
725
+ requested_workspace,
726
+ observed_pane_id: pane_id.as_str().to_string(),
727
+ observed_pane_workspace: pane_workspace,
728
+ endpoint,
729
+ },
730
+ ),
731
+ ));
732
+ }
733
+ Ok(VerifiedAmbientPaneAuthority {
734
+ pane_id,
735
+ observed,
736
+ endpoint,
737
+ tmux: tmux.to_string(),
738
+ })
739
+ }
740
+
741
+ fn ambient_tmux_endpoint_refusal(
742
+ workspace: &Path,
743
+ cause: pane_refusal::AmbientTmuxEndpointUnavailableCause,
744
+ ) -> pane_refusal::PaneAuthorityRefusal {
745
+ pane_refusal::PaneAuthorityRefusal::new(
746
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientTmuxEndpointUnavailable(
747
+ pane_refusal::AmbientTmuxEndpointUnavailableFacts {
748
+ requested_workspace: resolve_workspace_for_hash(workspace),
749
+ endpoint: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
750
+ },
751
+ ),
752
+ )
753
+ }
754
+
755
+ #[cfg(target_os = "macos")]
756
+ fn caller_controlling_tty() -> Result<u64, pane_refusal::CallerControllingTtyUnavailableCause> {
757
+ // `/dev/tty` proves that this process has a controlling terminal, but its
758
+ // own path and rdev remain the generic alias. Query the process table for
759
+ // the actual controlling device identity.
760
+ let _controlling_tty = std::fs::File::open("/dev/tty")
761
+ .map_err(|_| pane_refusal::CallerControllingTtyUnavailableCause::NoControllingTty)?;
762
+ let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
763
+ let info_size = std::mem::size_of::<libc::proc_bsdinfo>();
764
+ let read = unsafe {
765
+ libc::proc_pidinfo(
766
+ libc::getpid(),
767
+ libc::PROC_PIDTBSDINFO,
768
+ 0,
769
+ info.as_mut_ptr().cast(),
770
+ info_size as libc::c_int,
771
+ )
772
+ };
773
+ if read != info_size as libc::c_int {
774
+ return Err(pane_refusal::CallerControllingTtyUnavailableCause::DeviceIdentityUnresolvable);
775
+ }
776
+ Ok(u64::from(unsafe { info.assume_init() }.e_tdev))
777
+ }
778
+
779
+ #[cfg(target_os = "linux")]
780
+ fn caller_controlling_tty() -> Result<u64, pane_refusal::CallerControllingTtyUnavailableCause> {
781
+ let controlling_tty = std::fs::OpenOptions::new()
782
+ .read(true)
783
+ .custom_flags(libc::O_NOCTTY | libc::O_CLOEXEC)
784
+ .open("/dev/tty")
785
+ .map_err(|_| pane_refusal::CallerControllingTtyUnavailableCause::NoControllingTty)?;
786
+ let mut device = 0 as libc::c_uint;
787
+ let result = unsafe { libc::ioctl(controlling_tty.as_raw_fd(), libc::TIOCGDEV, &mut device) };
788
+ if result != 0 {
789
+ return Err(pane_refusal::CallerControllingTtyUnavailableCause::DeviceIdentityUnresolvable);
790
+ }
791
+ Ok(u64::from(device))
792
+ }
793
+
794
+ #[cfg(not(any(target_os = "macos", target_os = "linux")))]
795
+ fn caller_controlling_tty() -> Result<u64, pane_refusal::CallerControllingTtyUnavailableCause> {
796
+ Err(pane_refusal::CallerControllingTtyUnavailableCause::PlatformUnsupported)
797
+ }
798
+
799
+ #[cfg(unix)]
800
+ fn pane_tty_device(path: &str) -> Result<u64, pane_refusal::ObservedPaneTtyUnavailableCause> {
801
+ let pane_tty = std::fs::OpenOptions::new()
802
+ .read(true)
803
+ .custom_flags(libc::O_NOCTTY | libc::O_CLOEXEC)
804
+ .open(path)
805
+ .map_err(|_| pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyUnresolvable)?;
806
+ let metadata = pane_tty
807
+ .metadata()
808
+ .map_err(|_| pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyUnresolvable)?;
809
+ if !metadata.file_type().is_char_device() {
810
+ return Err(pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyNotCharacterDevice);
811
+ }
812
+ Ok(metadata.rdev())
813
+ }
814
+
815
+ #[cfg(not(unix))]
816
+ fn pane_tty_device(_path: &str) -> Result<u64, pane_refusal::ObservedPaneTtyUnavailableCause> {
817
+ Err(pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyUnresolvable)
818
+ }
819
+
269
820
  /// B5: the deterministic leader-session naming prefix IS the ownership truth source —
270
821
  /// shutdown's socket teardown spares sessions carrying it (no separate registry).
271
822
  pub const LEADER_SESSION_PREFIX: &str = "team-agent-leader-";
@@ -303,7 +854,7 @@ pub fn leader_session_name(provider: Provider, workspace: &Path) -> SessionName
303
854
  /// External / attach paths keep the stable `leader_session_name` —
304
855
  /// per-launch nonce would break `--attach-session <name>` reattach
305
856
  /// semantics.
306
- pub fn managed_leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
857
+ fn managed_leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
307
858
  let resolved = resolve_workspace_for_hash(workspace);
308
859
  let folder_raw = resolved
309
860
  .file_name()
@@ -322,6 +873,32 @@ pub fn managed_leader_session_name(provider: Provider, workspace: &Path) -> Sess
322
873
  ))
323
874
  }
324
875
 
876
+ fn managed_leader_session_for_launch(provider: Provider, workspace: &Path) -> SessionName {
877
+ let candidates = crate::transport_factory::tmux_workspace_transport(workspace)
878
+ .list_targets()
879
+ .unwrap_or_default()
880
+ .into_iter()
881
+ .map(|target| target.session.as_str().to_string());
882
+ managed_leader_session_from_candidates(provider, workspace, candidates)
883
+ }
884
+
885
+ fn managed_leader_session_from_candidates(
886
+ provider: Provider,
887
+ workspace: &Path,
888
+ candidates: impl IntoIterator<Item = String>,
889
+ ) -> SessionName {
890
+ let prefix = format!("{LEADER_SESSION_PREFIX}{}-", provider_wire(provider));
891
+ let existing = candidates
892
+ .into_iter()
893
+ .filter(|name| name.starts_with(&prefix))
894
+ .collect::<std::collections::BTreeSet<_>>();
895
+ existing
896
+ .into_iter()
897
+ .next()
898
+ .map(SessionName::new)
899
+ .unwrap_or_else(|| managed_leader_session_name(provider, workspace))
900
+ }
901
+
325
902
  fn start_argv(
326
903
  mode: LeaderStartMode,
327
904
  provider: Provider,
@@ -329,6 +906,7 @@ fn start_argv(
329
906
  workspace: &Path,
330
907
  session_name: Option<&SessionName>,
331
908
  leader_env: &BTreeMap<String, String>,
909
+ managed_client_attach_mode: Option<ManagedClientAttachMode>,
332
910
  ) -> Result<Vec<String>, LeaderError> {
333
911
  let provider_cmd = provider_command_name(provider).to_string();
334
912
  match mode {
@@ -343,7 +921,10 @@ fn start_argv(
343
921
  "managed leader session missing".to_string(),
344
922
  ));
345
923
  };
346
- managed_client_argv(workspace, session, provider)
924
+ let attach_mode = managed_client_attach_mode.ok_or_else(|| {
925
+ LeaderError::Start("managed client attach mode missing".to_string())
926
+ })?;
927
+ managed_client_argv(workspace, session, provider, attach_mode)
347
928
  }
348
929
  LeaderStartMode::AttachExisting => {
349
930
  let Some(session) = session_name else {
@@ -407,35 +988,34 @@ fn normalized_provider_args(provider_args: &[String]) -> impl Iterator<Item = St
407
988
  .cloned()
408
989
  }
409
990
 
410
- // 0.3.28 Step 2: `managed_team_session_name` deleted. Both managed and
411
- // external paths now compute the dedicated leader session via
412
- // `leader_session_name(provider, workspace)` directly. The old function
413
- // returned `team-<team_id>` which is the WORKER session — the structural
414
- // root of E49/E51/E53/E57-3/E60.
991
+ // 0.3.28 Step 2: `managed_team_session_name` deleted. Both paths use the
992
+ // dedicated leader-session namespace; managed launches select a live prefix
993
+ // candidate before falling back to the nonce helper, while external/attach
994
+ // launches use the stable `leader_session_name`.
415
995
 
416
996
  fn managed_client_argv(
417
997
  workspace: &Path,
418
998
  session: &SessionName,
419
999
  provider: Provider,
1000
+ attach_mode: ManagedClientAttachMode,
420
1001
  ) -> Result<Vec<String>, LeaderError> {
421
1002
  // 0.3.28 Step 2: leader window inside the dedicated leader session is
422
1003
  // named after `provider_wire(provider)` (e.g. `claude`, `codex`, `fake`),
423
1004
  // never the literal `leader`. Pre-0.3.28 this hardcoded `:leader`.
424
1005
  let target = format!("{}:{}", session.as_str(), provider_wire(provider));
425
- let argv = if std::env::var_os("TMUX").is_some() {
426
- vec![
1006
+ let argv = match attach_mode {
1007
+ ManagedClientAttachMode::SwitchClient => vec![
427
1008
  "tmux".to_string(),
428
1009
  "switch-client".to_string(),
429
1010
  "-t".to_string(),
430
1011
  target,
431
- ]
432
- } else {
433
- vec![
1012
+ ],
1013
+ ManagedClientAttachMode::AttachSession => vec![
434
1014
  "tmux".to_string(),
435
1015
  "attach-session".to_string(),
436
1016
  "-t".to_string(),
437
1017
  target,
438
- ]
1018
+ ],
439
1019
  };
440
1020
  Ok(TmuxBackend::argv_for_workspace(workspace, &argv))
441
1021
  }
@@ -443,6 +1023,14 @@ fn managed_client_argv(
443
1023
  fn execute_managed_leader_plan(
444
1024
  plan: &LeaderStartPlan,
445
1025
  workspace: &Path,
1026
+ ) -> Result<LeaderLaunchOutcome, LeaderError> {
1027
+ execute_managed_leader_plan_attempt(plan, workspace, true)
1028
+ }
1029
+
1030
+ fn execute_managed_leader_plan_attempt(
1031
+ plan: &LeaderStartPlan,
1032
+ workspace: &Path,
1033
+ allow_attach_recovery: bool,
446
1034
  ) -> Result<LeaderLaunchOutcome, LeaderError> {
447
1035
  let Some(session) = plan.session_name.as_ref() else {
448
1036
  return Err(LeaderError::Start(
@@ -460,19 +1048,76 @@ fn execute_managed_leader_plan(
460
1048
  // site. Semantics unchanged; managed leader hosting stays
461
1049
  // tmux-only per design §Batch 6.
462
1050
  let transport = crate::transport_factory::tmux_workspace_transport(workspace);
463
- let spawned = ensure_managed_leader_pane(&transport, session, window, plan, workspace)?;
464
- persist_managed_leader_binding(plan, workspace, &spawned)?;
1051
+ let session_existed_before = transport.has_session(session).map_err(|error| {
1052
+ LeaderError::Start(format!("managed leader session probe failed: {error}"))
1053
+ })?;
1054
+ let spawned = ensure_managed_leader_pane(
1055
+ &transport,
1056
+ session,
1057
+ window,
1058
+ plan,
1059
+ workspace,
1060
+ session_existed_before,
1061
+ )?;
1062
+ if let Err(error) = persist_managed_leader_binding(plan, workspace, &spawned) {
1063
+ cleanup_managed_leader_resources(
1064
+ &transport,
1065
+ session,
1066
+ &spawned,
1067
+ session_existed_before,
1068
+ workspace,
1069
+ "binding_persist_failed",
1070
+ );
1071
+ return Err(error);
1072
+ }
465
1073
  spawn_managed_provider_startup_prompt_handler(
466
1074
  plan.provider,
467
1075
  workspace.to_path_buf(),
468
1076
  spawned.pane_id.as_str().to_string(),
469
1077
  );
470
- let process = run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace)?;
1078
+ let process = match run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace) {
1079
+ Ok(process) => process,
1080
+ Err(error) => {
1081
+ cleanup_managed_leader_resources(
1082
+ &transport,
1083
+ session,
1084
+ &spawned,
1085
+ session_existed_before,
1086
+ workspace,
1087
+ "launcher_spawn_failed",
1088
+ );
1089
+ return Err(error);
1090
+ }
1091
+ };
471
1092
  let code = process.status.code();
472
1093
  if !process.status.success() {
473
- return Err(LeaderError::Start(leader_launcher_failure(&process)));
1094
+ cleanup_managed_leader_resources(
1095
+ &transport,
1096
+ session,
1097
+ &spawned,
1098
+ session_existed_before,
1099
+ workspace,
1100
+ "launcher_exit_failed",
1101
+ );
1102
+ let failure = LeaderError::Start(leader_launcher_failure(&process));
1103
+ if allow_attach_recovery {
1104
+ if let Ok(retry_plan) = managed_leader_attach_recovery_plan(plan, workspace) {
1105
+ return execute_managed_leader_plan_attempt(&retry_plan, workspace, false);
1106
+ }
1107
+ }
1108
+ return Err(failure);
1109
+ }
1110
+ if let Err(error) = ensure_managed_provider_live_after_attach(&transport, &spawned) {
1111
+ cleanup_managed_leader_resources(
1112
+ &transport,
1113
+ session,
1114
+ &spawned,
1115
+ session_existed_before,
1116
+ workspace,
1117
+ "provider_not_live_after_attach",
1118
+ );
1119
+ return Err(error);
474
1120
  }
475
- ensure_managed_provider_live_after_attach(&transport, &spawned)?;
476
1121
  Ok(LeaderLaunchOutcome {
477
1122
  status: LeaderLaunchStatus::Exited,
478
1123
  exit_code: code,
@@ -481,21 +1126,36 @@ fn execute_managed_leader_plan(
481
1126
  })
482
1127
  }
483
1128
 
1129
+ fn managed_leader_attach_recovery_plan(
1130
+ plan: &LeaderStartPlan,
1131
+ workspace: &Path,
1132
+ ) -> Result<LeaderStartPlan, LeaderError> {
1133
+ let session = managed_leader_session_for_launch(plan.provider, workspace);
1134
+ let attach_mode = if plan.argv.iter().any(|arg| arg == "switch-client") {
1135
+ ManagedClientAttachMode::SwitchClient
1136
+ } else {
1137
+ ManagedClientAttachMode::AttachSession
1138
+ };
1139
+ let argv = managed_client_argv(workspace, &session, plan.provider, attach_mode)?;
1140
+ let mut retry_plan = plan.clone();
1141
+ retry_plan.session_name = Some(session);
1142
+ retry_plan.argv = argv;
1143
+ Ok(retry_plan)
1144
+ }
1145
+
484
1146
  fn ensure_managed_leader_pane(
485
1147
  transport: &dyn Transport,
486
1148
  session: &SessionName,
487
1149
  window: &WindowName,
488
1150
  plan: &LeaderStartPlan,
489
1151
  workspace: &Path,
1152
+ session_existed_before: bool,
490
1153
  ) -> Result<SpawnResult, LeaderError> {
491
- // 0.4.10+ mirror-session fix v2 (option B): each managed launch gets
492
- // its OWN session name (via `managed_leader_session_name`'s nonce).
493
- // The historical "session already exists reuse pane" branch is
494
- // structurally unreachable now (the per-launch session never
495
- // preexists). If a caller bypasses the nonce path and reuses a name,
496
- // tmux's own duplicate-session error will surface — better than
497
- // silently attaching a second client.
498
- if transport.has_session(session).unwrap_or(false) {
1154
+ // A-22: a managed launch may reuse an existing provider leader session;
1155
+ // only a newly selected nonce session is absent before this probe.
1156
+ // Reusing the existing session adds one pane; a new session uses the
1157
+ // first-spawn path.
1158
+ if session_existed_before {
499
1159
  // 0.4.x (CR C-1 + C-2): leader env_unset reuses the worker
500
1160
  // provider_env_unsets (single source of truth) + spawn through the
501
1161
  // leader shell wrapper so provider exit returns to a shell, not
@@ -530,6 +1190,45 @@ fn ensure_managed_leader_pane(
530
1190
  }
531
1191
  }
532
1192
 
1193
+ fn cleanup_managed_leader_resources(
1194
+ transport: &dyn Transport,
1195
+ session: &SessionName,
1196
+ spawned: &SpawnResult,
1197
+ session_existed_before: bool,
1198
+ workspace: &Path,
1199
+ reason: &str,
1200
+ ) {
1201
+ let (action, result) = if session_existed_before {
1202
+ (
1203
+ "kill_pane",
1204
+ transport
1205
+ .kill_pane(&spawned.pane_id)
1206
+ .map(|_| "ok".to_string())
1207
+ .unwrap_or_else(|error| error.to_string()),
1208
+ )
1209
+ } else {
1210
+ (
1211
+ "kill_session",
1212
+ transport
1213
+ .kill_session(session)
1214
+ .map(|_| "ok".to_string())
1215
+ .unwrap_or_else(|error| error.to_string()),
1216
+ )
1217
+ };
1218
+ write_leader_startup_prompt_event(
1219
+ workspace,
1220
+ "leader.launcher.rollback",
1221
+ serde_json::json!({
1222
+ "reason": reason,
1223
+ "session": session.as_str(),
1224
+ "pane_id": spawned.pane_id.as_str(),
1225
+ "session_preexisted": session_existed_before,
1226
+ "action": action,
1227
+ "result": result,
1228
+ }),
1229
+ );
1230
+ }
1231
+
533
1232
  /// 0.4.x (CR C-1): leader provider env-unset list — SINGLE SOURCE OF TRUTH
534
1233
  /// reused from worker spawn (`profile_launch::provider_env_unsets`).
535
1234
  /// Audit grep guard: this function MUST be the only place in
@@ -538,7 +1237,7 @@ fn ensure_managed_leader_pane(
538
1237
  /// or the underlying `provider_env_unsets`. Use `AuthMode::Subscription` —
539
1238
  /// the leader is the user's interactive provider, never CompatibleApi/
540
1239
  /// OfficialApi which are worker-only auth modes today.
541
- pub fn leader_env_unset_for_provider(provider: Provider) -> Vec<String> {
1240
+ fn leader_env_unset_for_provider(provider: Provider) -> Vec<String> {
542
1241
  crate::lifecycle::profile_launch::provider_env_unsets(
543
1242
  provider,
544
1243
  crate::model::enums::AuthMode::Subscription,
@@ -652,7 +1351,7 @@ pub enum LeaderProviderHealth {
652
1351
  /// common interactive shells; missing entries here are false negatives
653
1352
  /// (shell looks like provider absent → health says Alive) which is the
654
1353
  /// safe default per the CR R6 conservative-Alive rule.
655
- pub fn is_interactive_shell_basename(name: &str) -> bool {
1354
+ fn is_interactive_shell_basename(name: &str) -> bool {
656
1355
  let trimmed = name.trim().to_ascii_lowercase();
657
1356
  let basename = std::path::Path::new(&trimmed)
658
1357
  .file_name()
@@ -721,6 +1420,11 @@ fn persist_managed_leader_binding(
721
1420
  let session = spawned.session.as_str().to_string();
722
1421
  let window = spawned.window.as_str().to_string();
723
1422
  let pane = spawned.pane_id.as_str().to_string();
1423
+ let attach_mode = if plan.argv.iter().any(|arg| arg == "switch-client") {
1424
+ "switch-client"
1425
+ } else {
1426
+ "attach-session"
1427
+ };
724
1428
  let receiver = serde_json::json!({
725
1429
  "mode": "direct_tmux",
726
1430
  "status": "attached",
@@ -783,7 +1487,7 @@ fn persist_managed_leader_binding(
783
1487
  "leader_client".to_string(),
784
1488
  serde_json::json!({
785
1489
  "diagnostic_only": true,
786
- "attach_mode": if std::env::var_os("TMUX").is_some() { "switch-client" } else { "attach-session" },
1490
+ "attach_mode": attach_mode,
787
1491
  "tmux": std::env::var("TMUX").ok(),
788
1492
  }),
789
1493
  );
@@ -821,17 +1525,15 @@ fn persist_managed_leader_binding(
821
1525
  fn persist_exec_provider_leader_binding(
822
1526
  plan: &LeaderStartPlan,
823
1527
  workspace: &Path,
1528
+ authority: &VerifiedAmbientPaneAuthority,
1529
+ is_external_leader: bool,
824
1530
  ) -> Result<(), LeaderError> {
825
1531
  let identity = plan
826
1532
  .identity
827
1533
  .as_ref()
828
1534
  .ok_or_else(|| LeaderError::Start("exec provider leader identity missing".to_string()))?;
829
- let pane = std::env::var("TMUX_PANE")
830
- .ok()
831
- .filter(|value| !value.trim().is_empty())
832
- .ok_or_else(|| LeaderError::Start("exec provider leader pane missing".to_string()))?;
833
- let pane_id = PaneId::new(pane.clone());
834
- let target = current_tmux_pane_info(&pane_id);
1535
+ let pane = authority.pane_id.as_str();
1536
+ let target = &authority.observed;
835
1537
  let mut state = crate::state::persist::load_runtime_state(workspace)
836
1538
  .unwrap_or_else(|_| serde_json::json!({}));
837
1539
  let owner_epoch = state
@@ -846,7 +1548,7 @@ fn persist_exec_provider_leader_binding(
846
1548
  .unwrap_or(0)
847
1549
  .saturating_add(1);
848
1550
  let now = chrono::Utc::now().to_rfc3339();
849
- let socket = crate::tmux_backend::socket_name_from_tmux_env();
1551
+ let socket = authority.endpoint.as_str();
850
1552
  let provider = serde_json::to_value(plan.provider)?;
851
1553
  let mut receiver = serde_json::json!({
852
1554
  "mode": "direct_tmux",
@@ -859,24 +1561,18 @@ fn persist_exec_provider_leader_binding(
859
1561
  "attached_at": now,
860
1562
  "discovery": "current_pane",
861
1563
  });
862
- if let Some(target) = target.as_ref() {
863
- if let Some(obj) = receiver.as_object_mut() {
1564
+ if let Some(obj) = receiver.as_object_mut() {
1565
+ obj.insert(
1566
+ "session_name".to_string(),
1567
+ serde_json::json!(target.session.as_str()),
1568
+ );
1569
+ if let Some(window_name) = target.window_name.as_ref() {
864
1570
  obj.insert(
865
- "session_name".to_string(),
866
- serde_json::json!(target.session.as_str()),
1571
+ "window_name".to_string(),
1572
+ serde_json::json!(window_name.as_str()),
867
1573
  );
868
- if let Some(window_name) = target.window_name.as_ref() {
869
- obj.insert(
870
- "window_name".to_string(),
871
- serde_json::json!(window_name.as_str()),
872
- );
873
- }
874
- }
875
- }
876
- if let Some(socket) = socket.as_ref() {
877
- if let Some(obj) = receiver.as_object_mut() {
878
- obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
879
1574
  }
1575
+ obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
880
1576
  }
881
1577
  let owner = serde_json::json!({
882
1578
  "pane_id": pane,
@@ -893,23 +1589,22 @@ fn persist_exec_provider_leader_binding(
893
1589
  "active_team_key".to_string(),
894
1590
  serde_json::json!(identity.team_id.as_str()),
895
1591
  );
896
- if let Some(target) = target.as_ref() {
897
- obj.insert(
898
- "session_name".to_string(),
899
- serde_json::json!(target.session.as_str()),
900
- );
901
- }
902
- if let Some(socket) = socket.as_ref() {
903
- obj.insert("tmux_endpoint".to_string(), serde_json::json!(socket));
904
- obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
905
- }
906
- obj.insert("is_external_leader".to_string(), serde_json::json!(false));
1592
+ obj.insert(
1593
+ "session_name".to_string(),
1594
+ serde_json::json!(target.session.as_str()),
1595
+ );
1596
+ obj.insert("tmux_endpoint".to_string(), serde_json::json!(socket));
1597
+ obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
1598
+ obj.insert(
1599
+ "is_external_leader".to_string(),
1600
+ serde_json::json!(is_external_leader),
1601
+ );
907
1602
  obj.insert(
908
1603
  "leader_client".to_string(),
909
1604
  serde_json::json!({
910
1605
  "diagnostic_only": true,
911
1606
  "attach_mode": "exec-provider",
912
- "tmux": std::env::var("TMUX").ok(),
1607
+ "tmux": authority.tmux.as_str(),
913
1608
  }),
914
1609
  );
915
1610
  }
@@ -954,13 +1649,9 @@ enum ExecProviderBinding {
954
1649
  fn classify_exec_provider_binding(
955
1650
  workspace: &Path,
956
1651
  plan: &LeaderStartPlan,
1652
+ authority: &VerifiedAmbientPaneAuthority,
957
1653
  ) -> Result<ExecProviderBinding, LeaderError> {
958
- let Some(pane) = std::env::var("TMUX_PANE")
959
- .ok()
960
- .filter(|value| !value.trim().is_empty())
961
- else {
962
- return Ok(ExecProviderBinding::Unbound);
963
- };
1654
+ let pane = authority.pane_id.as_str();
964
1655
  let state = match crate::state::persist::load_runtime_state(workspace) {
965
1656
  Ok(state) => state,
966
1657
  Err(_) => return Ok(ExecProviderBinding::Unbound),
@@ -996,12 +1687,11 @@ fn classify_exec_provider_binding(
996
1687
  if receiver_pane != pane {
997
1688
  return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
998
1689
  }
999
- let pane_id = PaneId::new(pane.clone());
1000
- let target = current_tmux_pane_info(&pane_id);
1001
- let leader_prefixed_session = target
1002
- .as_ref()
1003
- .map(|t| t.session.as_str().starts_with(LEADER_SESSION_PREFIX))
1004
- .unwrap_or(false);
1690
+ let leader_prefixed_session = authority
1691
+ .observed
1692
+ .session
1693
+ .as_str()
1694
+ .starts_with(LEADER_SESSION_PREFIX);
1005
1695
  if !leader_prefixed_session {
1006
1696
  return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
1007
1697
  }
@@ -1010,8 +1700,7 @@ fn classify_exec_provider_binding(
1010
1700
  .and_then(serde_json::Value::as_str)
1011
1701
  .filter(|s| !s.is_empty());
1012
1702
  if let Some(recorded_socket) = recorded_socket {
1013
- let caller_socket = crate::tmux_backend::socket_name_from_tmux_env();
1014
- if caller_socket.as_deref() != Some(recorded_socket) {
1703
+ if authority.endpoint != recorded_socket {
1015
1704
  return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
1016
1705
  }
1017
1706
  }
@@ -1027,16 +1716,13 @@ fn classify_exec_provider_binding(
1027
1716
  fn refresh_managed_leader_provider_binding(
1028
1717
  plan: &LeaderStartPlan,
1029
1718
  workspace: &Path,
1719
+ authority: &VerifiedAmbientPaneAuthority,
1030
1720
  ) -> Result<(), LeaderError> {
1031
1721
  let identity = plan.identity.as_ref().ok_or_else(|| {
1032
1722
  LeaderError::Start("managed leader re-entry identity missing".to_string())
1033
1723
  })?;
1034
- let pane = std::env::var("TMUX_PANE")
1035
- .ok()
1036
- .filter(|value| !value.trim().is_empty())
1037
- .ok_or_else(|| LeaderError::Start("managed leader re-entry pane missing".to_string()))?;
1038
- let pane_id = PaneId::new(pane.clone());
1039
- let target = current_tmux_pane_info(&pane_id);
1724
+ let pane = authority.pane_id.as_str();
1725
+ let target = &authority.observed;
1040
1726
  let mut state = crate::state::persist::load_runtime_state(workspace)
1041
1727
  .unwrap_or_else(|_| serde_json::json!({}));
1042
1728
  let team_key = identity.team_id.as_str();
@@ -1078,20 +1764,18 @@ fn refresh_managed_leader_provider_binding(
1078
1764
  obj.insert("owner_epoch".to_string(), serde_json::json!(existing_epoch));
1079
1765
  obj.entry("leader_session_uuid".to_string())
1080
1766
  .or_insert_with(|| serde_json::json!(identity.leader_session_uuid.as_str()));
1081
- if let Some(target) = target.as_ref() {
1767
+ obj.insert(
1768
+ "session_name".to_string(),
1769
+ serde_json::json!(target.session.as_str()),
1770
+ );
1771
+ if let Some(window_name) = target.window_name.as_ref() {
1082
1772
  obj.insert(
1083
- "session_name".to_string(),
1084
- serde_json::json!(target.session.as_str()),
1773
+ "window_name".to_string(),
1774
+ serde_json::json!(window_name.as_str()),
1085
1775
  );
1086
- if let Some(window_name) = target.window_name.as_ref() {
1087
- obj.insert(
1088
- "window_name".to_string(),
1089
- serde_json::json!(window_name.as_str()),
1090
- );
1091
- }
1092
- if let Some(pane_pid) = target.pane_pid {
1093
- obj.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
1094
- }
1776
+ }
1777
+ if let Some(pane_pid) = target.pane_pid {
1778
+ obj.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
1095
1779
  }
1096
1780
  }
1097
1781
  let mut owner = match existing_owner.as_object() {
@@ -1118,7 +1802,7 @@ fn refresh_managed_leader_provider_binding(
1118
1802
  serde_json::json!({
1119
1803
  "diagnostic_only": true,
1120
1804
  "attach_mode": "exec-provider",
1121
- "tmux": std::env::var("TMUX").ok(),
1805
+ "tmux": authority.tmux.as_str(),
1122
1806
  "reentry": true,
1123
1807
  }),
1124
1808
  );
@@ -1138,12 +1822,21 @@ fn refresh_managed_leader_provider_binding(
1138
1822
  Ok(())
1139
1823
  }
1140
1824
 
1141
- fn current_tmux_pane_info(pane_id: &PaneId) -> Option<crate::transport::PaneInfo> {
1142
- tmux_transport_for_current_pane()
1825
+ enum CurrentTmuxPaneInfoError {
1826
+ QueryFailed,
1827
+ PaneNotFound,
1828
+ }
1829
+
1830
+ fn current_tmux_pane_info(
1831
+ pane_id: &PaneId,
1832
+ endpoint: &str,
1833
+ ) -> Result<crate::transport::PaneInfo, CurrentTmuxPaneInfoError> {
1834
+ crate::transport_factory::tmux_endpoint_transport(endpoint)
1143
1835
  .list_targets()
1144
- .ok()?
1836
+ .map_err(|_| CurrentTmuxPaneInfoError::QueryFailed)?
1145
1837
  .into_iter()
1146
1838
  .find(|target| target.pane_id == *pane_id)
1839
+ .ok_or(CurrentTmuxPaneInfoError::PaneNotFound)
1147
1840
  }
1148
1841
 
1149
1842
  fn persist_external_leader_topology_marker(
@@ -1211,6 +1904,13 @@ fn run_leader_argv(
1211
1904
  "leader launch argv is empty".to_string(),
1212
1905
  ));
1213
1906
  };
1907
+ let diagnostics_path = launcher_diagnostics_path(workspace);
1908
+ if let Err(error) = write_launcher_diagnostics_header(&diagnostics_path, plan, argv) {
1909
+ return Err(LeaderError::Start(format!(
1910
+ "leader launcher diagnostics unavailable; startup_stage=diagnostics_create; launcher_diagnostics={}; error={error}",
1911
+ diagnostics_path.display()
1912
+ )));
1913
+ }
1214
1914
  // 0.4.x regression fix (env-leak scenario 1, in-tmux ExecProvider path):
1215
1915
  // the managed-tmux path got the provider env-unset block via the shell
1216
1916
  // wrapper (cb9c217), but the ExecProvider in-tmux path here is a direct
@@ -1223,11 +1923,15 @@ fn run_leader_argv(
1223
1923
  // leader_env_unset_for_provider).
1224
1924
  let env_unset = leader_env_unset_for_provider(plan.provider);
1225
1925
  let mut command = Command::new(program);
1226
- command
1227
- .args(argv.iter().skip(1))
1228
- .stdin(Stdio::inherit())
1229
- .stdout(Stdio::inherit());
1230
- let capture_stderr = plan.mode != LeaderStartMode::ExecProvider;
1926
+ command.args(argv.iter().skip(1)).stdin(Stdio::inherit());
1927
+ let capture_stdout = !std::io::stdout().is_terminal();
1928
+ command.stdout(if capture_stdout {
1929
+ Stdio::piped()
1930
+ } else {
1931
+ Stdio::inherit()
1932
+ });
1933
+ let capture_stderr =
1934
+ plan.mode != LeaderStartMode::ExecProvider || !std::io::stderr().is_terminal();
1231
1935
  command.stderr(if capture_stderr {
1232
1936
  Stdio::piped()
1233
1937
  } else {
@@ -1244,7 +1948,41 @@ fn run_leader_argv(
1244
1948
  for key in &env_unset {
1245
1949
  command.env_remove(key);
1246
1950
  }
1247
- let mut child = command.spawn()?;
1951
+ if plan.mode == LeaderStartMode::ManagedTmuxClient
1952
+ && argv.iter().any(|arg| arg == "attach-session")
1953
+ {
1954
+ command.env_remove("TMUX");
1955
+ command.env_remove("TMUX_PANE");
1956
+ }
1957
+ let mut child = command.spawn().map_err(|error| {
1958
+ LeaderError::Start(format!(
1959
+ "leader launcher spawn failed; startup_stage=spawn; launcher_diagnostics={}; error={error}",
1960
+ diagnostics_path.display()
1961
+ ))
1962
+ })?;
1963
+ append_launcher_diagnostics(&diagnostics_path, "startup_stage=spawned\n").map_err(|error| {
1964
+ LeaderError::Start(format!(
1965
+ "leader launcher diagnostics write failed; startup_stage=spawned; launcher_diagnostics={}; error={error}",
1966
+ diagnostics_path.display()
1967
+ ))
1968
+ })?;
1969
+ let stdout_reader = child.stdout.take().map(|mut child_stdout| {
1970
+ std::thread::spawn(move || {
1971
+ let mut captured = Vec::new();
1972
+ let mut buf = [0_u8; 4096];
1973
+ loop {
1974
+ let Ok(read) = child_stdout.read(&mut buf) else {
1975
+ break;
1976
+ };
1977
+ if read == 0 {
1978
+ break;
1979
+ }
1980
+ let _ = std::io::stdout().write_all(&buf[..read]);
1981
+ captured.extend_from_slice(&buf[..read]);
1982
+ }
1983
+ captured
1984
+ })
1985
+ });
1248
1986
  let stderr_reader = child.stderr.take().map(|mut child_stderr| {
1249
1987
  std::thread::spawn(move || {
1250
1988
  let mut captured = Vec::new();
@@ -1257,7 +1995,7 @@ fn run_leader_argv(
1257
1995
  break;
1258
1996
  }
1259
1997
  let _ = std::io::stderr().write_all(&buf[..read]);
1260
- push_bounded_stderr(&mut captured, &buf[..read]);
1998
+ captured.extend_from_slice(&buf[..read]);
1261
1999
  }
1262
2000
  captured
1263
2001
  })
@@ -1266,6 +2004,13 @@ fn run_leader_argv(
1266
2004
  spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
1267
2005
  }
1268
2006
  let status = child.wait().map_err(LeaderError::Io)?;
2007
+ let stdout = match stdout_reader {
2008
+ Some(reader) => match reader.join() {
2009
+ Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2010
+ Err(_) => String::new(),
2011
+ },
2012
+ None => String::new(),
2013
+ };
1269
2014
  let stderr = match stderr_reader {
1270
2015
  Some(reader) => match reader.join() {
1271
2016
  Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
@@ -1273,7 +2018,62 @@ fn run_leader_argv(
1273
2018
  },
1274
2019
  None => String::new(),
1275
2020
  };
1276
- Ok(LeaderProcessExit { status, stderr })
2021
+ append_launcher_diagnostics(
2022
+ &diagnostics_path,
2023
+ &format!(
2024
+ "startup_stage=exited\nexit_code={}\nchild_stdout={}\nchild_stderr={}\n",
2025
+ status
2026
+ .code()
2027
+ .map(|code| code.to_string())
2028
+ .unwrap_or_else(|| "signal".to_string()),
2029
+ crate::redaction::redact_external_text(&stdout),
2030
+ crate::redaction::redact_external_text(&stderr),
2031
+ ),
2032
+ )
2033
+ .map_err(|error| {
2034
+ LeaderError::Start(format!(
2035
+ "leader launcher diagnostics write failed; startup_stage=exited; launcher_diagnostics={}; error={error}",
2036
+ diagnostics_path.display()
2037
+ ))
2038
+ })?;
2039
+ Ok(LeaderProcessExit {
2040
+ status,
2041
+ stderr,
2042
+ diagnostics_path,
2043
+ })
2044
+ }
2045
+
2046
+ fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
2047
+ let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
2048
+ workspace.join(".team").join("logs").join(format!(
2049
+ "launcher-diagnostics-{stamp}-{}.log",
2050
+ std::process::id()
2051
+ ))
2052
+ }
2053
+
2054
+ fn write_launcher_diagnostics_header(
2055
+ path: &Path,
2056
+ plan: &LeaderStartPlan,
2057
+ argv: &[String],
2058
+ ) -> Result<(), std::io::Error> {
2059
+ if let Some(parent) = path.parent() {
2060
+ std::fs::create_dir_all(parent)?;
2061
+ }
2062
+ std::fs::write(
2063
+ path,
2064
+ format!(
2065
+ "startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
2066
+ plan.mode,
2067
+ provider_wire(plan.provider),
2068
+ crate::redaction::redact_external_text(&argv.join(" ")),
2069
+ ),
2070
+ )
2071
+ }
2072
+
2073
+ fn append_launcher_diagnostics(path: &Path, text: &str) -> Result<(), std::io::Error> {
2074
+ use std::io::Write as _;
2075
+ let mut file = std::fs::OpenOptions::new().append(true).open(path)?;
2076
+ file.write_all(text.as_bytes())
1277
2077
  }
1278
2078
 
1279
2079
  const LEADER_STDERR_LIMIT: usize = 8192;
@@ -1281,6 +2081,7 @@ const LEADER_STDERR_LIMIT: usize = 8192;
1281
2081
  struct LeaderProcessExit {
1282
2082
  status: std::process::ExitStatus,
1283
2083
  stderr: String,
2084
+ diagnostics_path: PathBuf,
1284
2085
  }
1285
2086
 
1286
2087
  fn push_bounded_stderr(captured: &mut Vec<u8>, chunk: &[u8]) {
@@ -1305,7 +2106,11 @@ fn leader_launcher_failure(process: &LeaderProcessExit) -> String {
1305
2106
  .code()
1306
2107
  .map(|code| code.to_string())
1307
2108
  .unwrap_or_else(|| "signal".to_string());
1308
- format_launcher_failure(&status, process.status.code().is_none(), &process.stderr)
2109
+ format!(
2110
+ "{}; startup_stage=exited; launcher_diagnostics={}",
2111
+ format_launcher_failure(&status, process.status.code().is_none(), &process.stderr),
2112
+ process.diagnostics_path.display()
2113
+ )
1309
2114
  }
1310
2115
 
1311
2116
  fn format_launcher_failure(status: &str, signaled: bool, raw_stderr: &str) -> String {
@@ -1399,7 +2204,7 @@ fn tmux_transport_for_current_pane() -> TmuxBackend {
1399
2204
  .unwrap_or_else(crate::transport_factory::tmux_default_transport)
1400
2205
  }
1401
2206
 
1402
- pub fn handle_exec_provider_startup_prompts(
2207
+ fn handle_exec_provider_startup_prompts(
1403
2208
  provider: Provider,
1404
2209
  workspace: &Path,
1405
2210
  pane_id: &str,
@@ -1543,9 +2348,10 @@ mod tests {
1543
2348
  };
1544
2349
 
1545
2350
  use super::{
1546
- ensure_managed_provider_live_after_attach, execute_leader_plan, format_launcher_failure,
1547
- handle_exec_provider_startup_prompts, push_bounded_stderr, shlex_quote,
1548
- LEADER_STDERR_LIMIT,
2351
+ ensure_managed_provider_live_after_attach, execute_leader_plan_after_ambient_authority,
2352
+ format_launcher_failure, handle_exec_provider_startup_prompts,
2353
+ is_interactive_shell_basename, push_bounded_stderr, shlex_quote,
2354
+ VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
1549
2355
  };
1550
2356
 
1551
2357
  struct ScriptedTransport {
@@ -1857,7 +2663,7 @@ mod tests {
1857
2663
  detached: false,
1858
2664
  };
1859
2665
 
1860
- let outcome = execute_leader_plan(&plan, &workspace)
2666
+ let outcome = execute_leader_plan_after_ambient_authority(&plan, &workspace, None)
1861
2667
  .expect("external marker must be present before provider argv runs");
1862
2668
 
1863
2669
  assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
@@ -1914,8 +2720,27 @@ mod tests {
1914
2720
  detached: false,
1915
2721
  };
1916
2722
 
1917
- let outcome = execute_leader_plan(&plan, &workspace)
1918
- .expect("current pane binding must be present before provider argv runs");
2723
+ let authority = VerifiedAmbientPaneAuthority {
2724
+ pane_id: PaneId::new("%77"),
2725
+ observed: PaneInfo {
2726
+ pane_id: PaneId::new("%77"),
2727
+ session: SessionName::new("team-agent-leader-current"),
2728
+ window_index: Some(0),
2729
+ window_name: Some(WindowName::new("codex")),
2730
+ pane_index: Some(0),
2731
+ tty: Some("/dev/ttys077".to_string()),
2732
+ current_command: Some("codex".to_string()),
2733
+ current_path: Some(workspace.clone()),
2734
+ active: true,
2735
+ pane_pid: None,
2736
+ leader_env: BTreeMap::new(),
2737
+ },
2738
+ endpoint: "/private/tmp/tmux-501/default".to_string(),
2739
+ tmux: "/private/tmp/tmux-501/default,123,0".to_string(),
2740
+ };
2741
+ let outcome =
2742
+ execute_leader_plan_after_ambient_authority(&plan, &workspace, Some(&authority))
2743
+ .expect("current pane binding must be present before provider argv runs");
1919
2744
 
1920
2745
  assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
1921
2746
  let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
@@ -1968,12 +2793,26 @@ mod tests {
1968
2793
  assert_eq!(sent[0].1, vec![Key::Enter]);
1969
2794
  }
1970
2795
 
2796
+ #[test]
2797
+ fn interactive_shell_basename_recognizes_supported_shells() {
2798
+ let shells = [
2799
+ "zsh", "bash", "sh", "fish", "dash", "ksh", "tcsh", "csh", "ash", "mksh", "yash",
2800
+ "elvish", "nu", "nushell", "xonsh",
2801
+ ];
2802
+ for shell in shells {
2803
+ assert!(is_interactive_shell_basename(shell));
2804
+ assert!(is_interactive_shell_basename(&shell.to_uppercase()));
2805
+ }
2806
+ for provider in ["claude", "codex", "copilot", "gemini"] {
2807
+ assert!(!is_interactive_shell_basename(provider));
2808
+ }
2809
+ }
2810
+
1971
2811
  // ═══════════════════════════════════════════════════════════════════
1972
2812
  // 0.4.10+ mirror-session fix v2 (option B: independent session per launch).
1973
2813
  //
1974
- // managed_leader_session_name appends a per-launch nonce so each
1975
- // `team-agent <provider>` entry in the same workspace creates its own
1976
- // tmux session, matching `tmux new-session + claude` semantics.
2814
+ // The managed selector reuses a live provider leader prefix when present;
2815
+ // its no-candidate fallback appends a per-launch nonce.
1977
2816
  // ═══════════════════════════════════════════════════════════════════
1978
2817
 
1979
2818
  #[test]
@@ -2046,37 +2885,84 @@ mod tests {
2046
2885
  let _ = std::fs::remove_dir_all(&workspace);
2047
2886
  }
2048
2887
 
2049
- /// CR C-5 source grep guard: managed mode passes the nonce session
2050
- /// name through `leader_start_plan` so the per-launch independence
2051
- /// reaches `ensure_managed_leader_pane`. External/attach paths must
2052
- /// keep `leader_session_name`.
2888
+ // Tombstone for `managed_path_uses_nonce_session_name_grep_guard`: A-22
2889
+ // replaced that implementation-shape assertion with the contract guard
2890
+ // below. Keep the old name here for audit/search provenance.
2053
2891
  #[test]
2054
- fn managed_path_uses_nonce_session_name_grep_guard() {
2892
+ fn a22_launch_session_reuse_contract() {
2055
2893
  let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2056
2894
  let start_rs = manifest.join("src").join("leader").join("start.rs");
2057
2895
  let contents = std::fs::read_to_string(&start_rs).expect("read leader/start.rs");
2058
- // leader_start_plan must dispatch to managed_leader_session_name
2059
- // on the managed path.
2060
2896
  let start = contents
2061
- .find("pub fn leader_start_plan(")
2062
- .expect("leader_start_plan must exist");
2897
+ .find("fn leader_start_plan_with_ambient_authority(")
2898
+ .expect("leader_start_plan_with_ambient_authority must exist");
2063
2899
  let end = contents[start + 1..]
2064
- .find("\npub fn ")
2065
- .or_else(|| contents[start + 1..].find("\nfn "))
2066
- .map(|p| start + 1 + p)
2900
+ .find("\nfn ")
2901
+ .map(|offset| start + 1 + offset)
2067
2902
  .unwrap_or(contents.len());
2068
2903
  let body = &contents[start..end];
2069
2904
  assert!(
2070
- body.contains("managed_leader_session_name(provider, workspace)"),
2071
- "leader_start_plan must call managed_leader_session_name on \
2072
- the managed path (mirror-session fix v2); body excerpt: {body}"
2905
+ body.contains("managed_leader_session_for_launch(provider, workspace)"),
2906
+ "managed launch must select from the A-22 candidate contract; body excerpt: {body}"
2073
2907
  );
2074
- // The external_path branch must still use the stable name.
2075
2908
  assert!(
2076
2909
  body.contains("leader_session_name(provider, workspace)"),
2077
2910
  "leader_start_plan must keep leader_session_name on the \
2078
2911
  external/attach paths; body excerpt: {body}"
2079
2912
  );
2913
+ let workspace =
2914
+ std::env::temp_dir().join(format!("ta_rs_a22_contract_{}", std::process::id()));
2915
+ let reused = super::managed_leader_session_from_candidates(
2916
+ Provider::ClaudeCode,
2917
+ &workspace,
2918
+ vec![
2919
+ "team-worker-not-a-leader".to_string(),
2920
+ "team-agent-leader-claude_code-z".to_string(),
2921
+ "team-agent-leader-codex-other".to_string(),
2922
+ "team-agent-leader-claude_code-a".to_string(),
2923
+ ],
2924
+ );
2925
+ assert_eq!(
2926
+ reused.as_str(),
2927
+ "team-agent-leader-claude_code-a",
2928
+ "managed launch must deterministically reuse a matching leader prefix session"
2929
+ );
2930
+ let _ = std::fs::remove_dir_all(workspace);
2931
+ }
2932
+
2933
+ #[test]
2934
+ fn a22_launch_session_without_candidate_gets_nonce() {
2935
+ let workspace =
2936
+ std::env::temp_dir().join(format!("ta_rs_a22_nonce_{}", std::process::id()));
2937
+ let fresh = super::managed_leader_session_from_candidates(
2938
+ Provider::ClaudeCode,
2939
+ &workspace,
2940
+ std::iter::empty(),
2941
+ );
2942
+ assert!(
2943
+ fresh.as_str().starts_with("team-agent-leader-claude_code-"),
2944
+ "no candidate must create a leader-prefixed managed session: {}",
2945
+ fresh.as_str()
2946
+ );
2947
+ assert_ne!(
2948
+ fresh.as_str(),
2949
+ super::leader_session_name(Provider::ClaudeCode, &workspace).as_str(),
2950
+ "managed fallback must carry a per-launch nonce"
2951
+ );
2952
+ let _ = std::fs::remove_dir_all(workspace);
2953
+ }
2954
+
2955
+ #[test]
2956
+ fn a22_external_attach_session_name_remains_stable() {
2957
+ let workspace =
2958
+ std::env::temp_dir().join(format!("ta_rs_a22_external_{}", std::process::id()));
2959
+ let stable = super::leader_session_name(Provider::ClaudeCode, &workspace);
2960
+ assert_eq!(
2961
+ stable.as_str(),
2962
+ super::leader_session_name(Provider::ClaudeCode, &workspace).as_str(),
2963
+ "external/attach paths must keep the stable workspace-keyed session name"
2964
+ );
2965
+ let _ = std::fs::remove_dir_all(workspace);
2080
2966
  }
2081
2967
 
2082
2968
  #[test]