@team-agent/installer 0.5.61 → 0.5.63

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 +15 -866
  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 +139 -375
  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 -49
  49. package/crates/team-agent/src/coordinator/tick.rs +23 -40
  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 -66
  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 -15
  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 -13
  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
@@ -1,4 +1,5 @@
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};
@@ -26,7 +27,7 @@ use super::{
26
27
  LeaderStartMode, LeaderStartPlan,
27
28
  };
28
29
 
29
- // ── leader::start — leader_start_plan / start_leader / session 名 ──
30
+ // ── leader::start — leader_start_plan / session 名 ──
30
31
 
31
32
  pub(crate) struct PreparedLeaderStart {
32
33
  plan: LeaderStartPlan,
@@ -74,7 +75,7 @@ impl From<LeaderError> for PrepareLeaderStartError {
74
75
 
75
76
  /// `leader_start_plan`(card §46;`__init__.py:82`)。计算 leader 启动计划
76
77
  /// (exec in-TMUX / new tmux session / attach existing)。provider 未安装 → `Err(Start)`。
77
- pub fn leader_start_plan(
78
+ pub(crate) fn leader_start_plan(
78
79
  provider: Provider,
79
80
  provider_args: &[String],
80
81
  workspace: &Path,
@@ -232,20 +233,16 @@ fn leader_start_plan_with_ambient_authority(
232
233
  // `managed_team_session_name(identity) = team-<team_id>` which is the
233
234
  // worker session — that co-location is the structural root of
234
235
  // E49/E51/E53/E57-3/E60.
235
- // 0.4.10+ mirror-session fix v2 (option B): managed-mode session name
236
- // gets a per-launch nonce so each `team-agent <provider>` entry in the
237
- // same workspace creates its OWN tmux session matching the user
238
- // expectation that `tmux new-session + claude` is independent every
239
- // time. Pre-fix the managed path used the workspace-keyed name (same
240
- // session for every entry) and silently attached the second client →
241
- // UI mirror. The external/attach paths keep the stable workspace-keyed
242
- // 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.
243
240
  let session_name = if external_path {
244
241
  attach_session
245
242
  .cloned()
246
243
  .or_else(|| Some(leader_session_name(provider, workspace)))
247
244
  } else {
248
- Some(managed_leader_session_name(provider, workspace))
245
+ Some(managed_leader_session_for_launch(provider, workspace))
249
246
  };
250
247
  if !in_tmux {
251
248
  ensure_tmux_installed()?;
@@ -348,66 +345,6 @@ pub(crate) fn leader_env_for_identity(
348
345
  leader_env
349
346
  }
350
347
 
351
- /// `start_leader`(card §46;`__init__.py:60`)。计算并执行 leader 启动计划(spawn + 信号处理)。
352
- /// 进程退出后 `Err`/退出码经 caller 处理(此处返 `Result` 替代 Python 的 `SystemExit`)。
353
- pub fn start_leader(
354
- provider: Provider,
355
- provider_args: &[String],
356
- workspace: &Path,
357
- attach_existing: bool,
358
- confirm_attach: bool,
359
- attach_session: Option<&SessionName>,
360
- external_leader: bool,
361
- ) -> Result<(), LeaderError> {
362
- let prepared = prepare_leader_start(
363
- provider,
364
- provider_args,
365
- workspace,
366
- attach_existing,
367
- confirm_attach,
368
- attach_session,
369
- external_leader,
370
- )
371
- .map_err(PrepareLeaderStartError::into_leader_error)?;
372
- let plan = prepared.plan();
373
- crate::event_log::EventLog::new(workspace).write(
374
- super::LeaderEvent::LeaderStart.name(),
375
- serde_json::json!({
376
- "provider": super::helpers::provider_wire(plan.provider),
377
- "mode": serde_json::to_value(plan.mode)?,
378
- "session_name": plan.session_name.as_ref().map(|s| s.as_str().to_string()),
379
- }),
380
- )?;
381
- execute_prepared_leader_start(&prepared, workspace).map(|_| ())
382
- }
383
-
384
- /// Execute a precomputed leader launch plan.
385
- ///
386
- /// S0 exposes the seam and return model only. Lane 2 owns the real provider/tmux
387
- /// execution and workspace-socket enforcement.
388
- pub fn execute_leader_plan(
389
- plan: &LeaderStartPlan,
390
- workspace: &Path,
391
- ) -> Result<LeaderLaunchOutcome, LeaderError> {
392
- let ambient_authority = if plan.mode == LeaderStartMode::ExecProvider {
393
- match ambient_pane_authority_preflight(workspace) {
394
- Ok(Some(authority)) => Some(authority),
395
- Ok(None) => {
396
- return Err(LeaderError::Validation(
397
- "exec provider ambient pane authority missing".to_string(),
398
- ));
399
- }
400
- Err(PrepareLeaderStartError::PaneAuthorityRefused(refusal)) => {
401
- return Ok(LeaderLaunchOutcome::not_started(refusal.reason().as_str()));
402
- }
403
- Err(PrepareLeaderStartError::Leader(error)) => return Err(error),
404
- }
405
- } else {
406
- None
407
- };
408
- execute_leader_plan_after_ambient_authority(plan, workspace, ambient_authority.as_ref())
409
- }
410
-
411
348
  pub(crate) fn execute_prepared_leader_start(
412
349
  prepared: &PreparedLeaderStart,
413
350
  workspace: &Path,
@@ -917,7 +854,7 @@ pub fn leader_session_name(provider: Provider, workspace: &Path) -> SessionName
917
854
  /// External / attach paths keep the stable `leader_session_name` —
918
855
  /// per-launch nonce would break `--attach-session <name>` reattach
919
856
  /// semantics.
920
- pub fn managed_leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
857
+ fn managed_leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
921
858
  let resolved = resolve_workspace_for_hash(workspace);
922
859
  let folder_raw = resolved
923
860
  .file_name()
@@ -936,6 +873,32 @@ pub fn managed_leader_session_name(provider: Provider, workspace: &Path) -> Sess
936
873
  ))
937
874
  }
938
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
+
939
902
  fn start_argv(
940
903
  mode: LeaderStartMode,
941
904
  provider: Provider,
@@ -1025,11 +988,10 @@ fn normalized_provider_args(provider_args: &[String]) -> impl Iterator<Item = St
1025
988
  .cloned()
1026
989
  }
1027
990
 
1028
- // 0.3.28 Step 2: `managed_team_session_name` deleted. Both managed and
1029
- // external paths now compute the dedicated leader session via
1030
- // `leader_session_name(provider, workspace)` directly. The old function
1031
- // returned `team-<team_id>` which is the WORKER session — the structural
1032
- // 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`.
1033
995
 
1034
996
  fn managed_client_argv(
1035
997
  workspace: &Path,
@@ -1061,6 +1023,14 @@ fn managed_client_argv(
1061
1023
  fn execute_managed_leader_plan(
1062
1024
  plan: &LeaderStartPlan,
1063
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,
1064
1034
  ) -> Result<LeaderLaunchOutcome, LeaderError> {
1065
1035
  let Some(session) = plan.session_name.as_ref() else {
1066
1036
  return Err(LeaderError::Start(
@@ -1078,19 +1048,76 @@ fn execute_managed_leader_plan(
1078
1048
  // site. Semantics unchanged; managed leader hosting stays
1079
1049
  // tmux-only per design §Batch 6.
1080
1050
  let transport = crate::transport_factory::tmux_workspace_transport(workspace);
1081
- let spawned = ensure_managed_leader_pane(&transport, session, window, plan, workspace)?;
1082
- 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
+ }
1083
1073
  spawn_managed_provider_startup_prompt_handler(
1084
1074
  plan.provider,
1085
1075
  workspace.to_path_buf(),
1086
1076
  spawned.pane_id.as_str().to_string(),
1087
1077
  );
1088
- 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
+ };
1089
1092
  let code = process.status.code();
1090
1093
  if !process.status.success() {
1091
- 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);
1092
1120
  }
1093
- ensure_managed_provider_live_after_attach(&transport, &spawned)?;
1094
1121
  Ok(LeaderLaunchOutcome {
1095
1122
  status: LeaderLaunchStatus::Exited,
1096
1123
  exit_code: code,
@@ -1099,21 +1126,36 @@ fn execute_managed_leader_plan(
1099
1126
  })
1100
1127
  }
1101
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
+
1102
1146
  fn ensure_managed_leader_pane(
1103
1147
  transport: &dyn Transport,
1104
1148
  session: &SessionName,
1105
1149
  window: &WindowName,
1106
1150
  plan: &LeaderStartPlan,
1107
1151
  workspace: &Path,
1152
+ session_existed_before: bool,
1108
1153
  ) -> Result<SpawnResult, LeaderError> {
1109
- // 0.4.10+ mirror-session fix v2 (option B): each managed launch gets
1110
- // its OWN session name (via `managed_leader_session_name`'s nonce).
1111
- // The historical "session already exists reuse pane" branch is
1112
- // structurally unreachable now (the per-launch session never
1113
- // preexists). If a caller bypasses the nonce path and reuses a name,
1114
- // tmux's own duplicate-session error will surface — better than
1115
- // silently attaching a second client.
1116
- 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 {
1117
1159
  // 0.4.x (CR C-1 + C-2): leader env_unset reuses the worker
1118
1160
  // provider_env_unsets (single source of truth) + spawn through the
1119
1161
  // leader shell wrapper so provider exit returns to a shell, not
@@ -1148,6 +1190,45 @@ fn ensure_managed_leader_pane(
1148
1190
  }
1149
1191
  }
1150
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
+
1151
1232
  /// 0.4.x (CR C-1): leader provider env-unset list — SINGLE SOURCE OF TRUTH
1152
1233
  /// reused from worker spawn (`profile_launch::provider_env_unsets`).
1153
1234
  /// Audit grep guard: this function MUST be the only place in
@@ -1156,7 +1237,7 @@ fn ensure_managed_leader_pane(
1156
1237
  /// or the underlying `provider_env_unsets`. Use `AuthMode::Subscription` —
1157
1238
  /// the leader is the user's interactive provider, never CompatibleApi/
1158
1239
  /// OfficialApi which are worker-only auth modes today.
1159
- pub fn leader_env_unset_for_provider(provider: Provider) -> Vec<String> {
1240
+ fn leader_env_unset_for_provider(provider: Provider) -> Vec<String> {
1160
1241
  crate::lifecycle::profile_launch::provider_env_unsets(
1161
1242
  provider,
1162
1243
  crate::model::enums::AuthMode::Subscription,
@@ -1270,7 +1351,7 @@ pub enum LeaderProviderHealth {
1270
1351
  /// common interactive shells; missing entries here are false negatives
1271
1352
  /// (shell looks like provider absent → health says Alive) which is the
1272
1353
  /// safe default per the CR R6 conservative-Alive rule.
1273
- pub fn is_interactive_shell_basename(name: &str) -> bool {
1354
+ fn is_interactive_shell_basename(name: &str) -> bool {
1274
1355
  let trimmed = name.trim().to_ascii_lowercase();
1275
1356
  let basename = std::path::Path::new(&trimmed)
1276
1357
  .file_name()
@@ -1823,6 +1904,13 @@ fn run_leader_argv(
1823
1904
  "leader launch argv is empty".to_string(),
1824
1905
  ));
1825
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
+ }
1826
1914
  // 0.4.x regression fix (env-leak scenario 1, in-tmux ExecProvider path):
1827
1915
  // the managed-tmux path got the provider env-unset block via the shell
1828
1916
  // wrapper (cb9c217), but the ExecProvider in-tmux path here is a direct
@@ -1835,11 +1923,15 @@ fn run_leader_argv(
1835
1923
  // leader_env_unset_for_provider).
1836
1924
  let env_unset = leader_env_unset_for_provider(plan.provider);
1837
1925
  let mut command = Command::new(program);
1838
- command
1839
- .args(argv.iter().skip(1))
1840
- .stdin(Stdio::inherit())
1841
- .stdout(Stdio::inherit());
1842
- 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();
1843
1935
  command.stderr(if capture_stderr {
1844
1936
  Stdio::piped()
1845
1937
  } else {
@@ -1862,7 +1954,35 @@ fn run_leader_argv(
1862
1954
  command.env_remove("TMUX");
1863
1955
  command.env_remove("TMUX_PANE");
1864
1956
  }
1865
- let mut child = command.spawn()?;
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
+ });
1866
1986
  let stderr_reader = child.stderr.take().map(|mut child_stderr| {
1867
1987
  std::thread::spawn(move || {
1868
1988
  let mut captured = Vec::new();
@@ -1875,7 +1995,7 @@ fn run_leader_argv(
1875
1995
  break;
1876
1996
  }
1877
1997
  let _ = std::io::stderr().write_all(&buf[..read]);
1878
- push_bounded_stderr(&mut captured, &buf[..read]);
1998
+ captured.extend_from_slice(&buf[..read]);
1879
1999
  }
1880
2000
  captured
1881
2001
  })
@@ -1884,6 +2004,13 @@ fn run_leader_argv(
1884
2004
  spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
1885
2005
  }
1886
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
+ };
1887
2014
  let stderr = match stderr_reader {
1888
2015
  Some(reader) => match reader.join() {
1889
2016
  Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
@@ -1891,7 +2018,62 @@ fn run_leader_argv(
1891
2018
  },
1892
2019
  None => String::new(),
1893
2020
  };
1894
- 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())
1895
2077
  }
1896
2078
 
1897
2079
  const LEADER_STDERR_LIMIT: usize = 8192;
@@ -1899,6 +2081,7 @@ const LEADER_STDERR_LIMIT: usize = 8192;
1899
2081
  struct LeaderProcessExit {
1900
2082
  status: std::process::ExitStatus,
1901
2083
  stderr: String,
2084
+ diagnostics_path: PathBuf,
1902
2085
  }
1903
2086
 
1904
2087
  fn push_bounded_stderr(captured: &mut Vec<u8>, chunk: &[u8]) {
@@ -1923,7 +2106,11 @@ fn leader_launcher_failure(process: &LeaderProcessExit) -> String {
1923
2106
  .code()
1924
2107
  .map(|code| code.to_string())
1925
2108
  .unwrap_or_else(|| "signal".to_string());
1926
- 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
+ )
1927
2114
  }
1928
2115
 
1929
2116
  fn format_launcher_failure(status: &str, signaled: bool, raw_stderr: &str) -> String {
@@ -2017,7 +2204,7 @@ fn tmux_transport_for_current_pane() -> TmuxBackend {
2017
2204
  .unwrap_or_else(crate::transport_factory::tmux_default_transport)
2018
2205
  }
2019
2206
 
2020
- pub fn handle_exec_provider_startup_prompts(
2207
+ fn handle_exec_provider_startup_prompts(
2021
2208
  provider: Provider,
2022
2209
  workspace: &Path,
2023
2210
  pane_id: &str,
@@ -2161,9 +2348,9 @@ mod tests {
2161
2348
  };
2162
2349
 
2163
2350
  use super::{
2164
- ensure_managed_provider_live_after_attach, execute_leader_plan,
2165
- execute_leader_plan_after_ambient_authority, format_launcher_failure,
2166
- handle_exec_provider_startup_prompts, push_bounded_stderr, shlex_quote,
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,
2167
2354
  VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
2168
2355
  };
2169
2356
 
@@ -2606,12 +2793,26 @@ mod tests {
2606
2793
  assert_eq!(sent[0].1, vec![Key::Enter]);
2607
2794
  }
2608
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
+
2609
2811
  // ═══════════════════════════════════════════════════════════════════
2610
2812
  // 0.4.10+ mirror-session fix v2 (option B: independent session per launch).
2611
2813
  //
2612
- // managed_leader_session_name appends a per-launch nonce so each
2613
- // `team-agent <provider>` entry in the same workspace creates its own
2614
- // 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.
2615
2816
  // ═══════════════════════════════════════════════════════════════════
2616
2817
 
2617
2818
  #[test]
@@ -2684,37 +2885,84 @@ mod tests {
2684
2885
  let _ = std::fs::remove_dir_all(&workspace);
2685
2886
  }
2686
2887
 
2687
- /// CR C-5 source grep guard: managed mode passes the nonce session
2688
- /// name through `leader_start_plan` so the per-launch independence
2689
- /// reaches `ensure_managed_leader_pane`. External/attach paths must
2690
- /// 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.
2691
2891
  #[test]
2692
- fn managed_path_uses_nonce_session_name_grep_guard() {
2892
+ fn a22_launch_session_reuse_contract() {
2693
2893
  let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2694
2894
  let start_rs = manifest.join("src").join("leader").join("start.rs");
2695
2895
  let contents = std::fs::read_to_string(&start_rs).expect("read leader/start.rs");
2696
- // leader_start_plan must dispatch to managed_leader_session_name
2697
- // on the managed path.
2698
2896
  let start = contents
2699
- .find("pub fn leader_start_plan(")
2700
- .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");
2701
2899
  let end = contents[start + 1..]
2702
- .find("\npub fn ")
2703
- .or_else(|| contents[start + 1..].find("\nfn "))
2704
- .map(|p| start + 1 + p)
2900
+ .find("\nfn ")
2901
+ .map(|offset| start + 1 + offset)
2705
2902
  .unwrap_or(contents.len());
2706
2903
  let body = &contents[start..end];
2707
2904
  assert!(
2708
- body.contains("managed_leader_session_name(provider, workspace)"),
2709
- "leader_start_plan must call managed_leader_session_name on \
2710
- 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}"
2711
2907
  );
2712
- // The external_path branch must still use the stable name.
2713
2908
  assert!(
2714
2909
  body.contains("leader_session_name(provider, workspace)"),
2715
2910
  "leader_start_plan must keep leader_session_name on the \
2716
2911
  external/attach paths; body excerpt: {body}"
2717
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);
2718
2966
  }
2719
2967
 
2720
2968
  #[test]