@team-agent/installer 0.5.77 → 0.5.78

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 (83) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +161 -29
  4. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  5. package/crates/team-agent/src/cli/emit.rs +24 -8
  6. package/crates/team-agent/src/cli/mod.rs +543 -98
  7. package/crates/team-agent/src/cli/models.rs +362 -32
  8. package/crates/team-agent/src/cli/named_address.rs +19 -5
  9. package/crates/team-agent/src/cli/profile.rs +35 -4
  10. package/crates/team-agent/src/cli/spec.rs +7 -2
  11. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  12. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  13. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  14. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  15. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  16. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  17. package/crates/team-agent/src/cli/types.rs +1 -0
  18. package/crates/team-agent/src/leader/lease.rs +492 -33
  19. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  20. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  21. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  22. package/crates/team-agent/src/leader/start.rs +151 -14
  23. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  24. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  25. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  26. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  27. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  28. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
  29. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  30. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  31. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  32. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  33. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  34. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  38. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  39. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  40. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  41. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  42. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  43. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  44. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  45. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  46. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  47. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  48. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  49. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  50. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  51. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  52. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  53. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  54. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  55. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  56. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  59. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  60. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  61. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  63. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  64. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  65. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  66. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  67. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  68. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  69. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  70. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  71. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  72. package/crates/team-agent/src/state/persist.rs +97 -1
  73. package/crates/team-agent/src/state/repository.rs +29 -0
  74. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  75. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  76. package/crates/team-agent/src/tmux_backend.rs +208 -118
  77. package/crates/team-agent/src/transport.rs +72 -0
  78. package/crates/team-agent/src/transport_factory.rs +53 -0
  79. package/package.json +4 -5
  80. package/skills/team-agent/SKILL.md +45 -17
  81. package/skills/team-agent/command-coverage.json +4 -29
  82. package/npm/check-skill-links.mjs +0 -88
  83. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -142,7 +142,28 @@ pub fn bind_owner_from_caller_pane(
142
142
  .as_ref()
143
143
  .and_then(|info| info.current_command.clone())
144
144
  .unwrap_or_else(|| tmux_pane_current_command(workspace, &pane).unwrap_or_default());
145
- let provider = bind_provider_from_env_or_pane(&caller_current_command, caller_info);
145
+ let Some(provider) = bind_provider_from_env_or_pane(&caller_current_command, caller_info) else {
146
+ let hint = "set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; unknown commands are not Codex";
147
+ event_log.write(
148
+ LeaderEvent::OwnerBindRefused.name(),
149
+ json!({
150
+ "reason": serde_json::to_value(LeaseReason::CallerNotLeaderShaped)?,
151
+ "caller_pane_id": pane,
152
+ "caller_current_command": caller_current_command.clone(),
153
+ "team_id": team_id.as_str(),
154
+ "hint": hint,
155
+ }),
156
+ )?;
157
+ return Ok(OwnerBindResult {
158
+ ok: false,
159
+ owner: None,
160
+ caller_pane_id: PaneId::new(pane),
161
+ caller_current_command,
162
+ team_id: team_id.clone(),
163
+ reason: Some(LeaseReason::CallerNotLeaderShaped),
164
+ hint: Some(hint.to_string()),
165
+ });
166
+ };
146
167
  let machine_fingerprint = std::env::var("TEAM_AGENT_MACHINE_FINGERPRINT").unwrap_or_default();
147
168
  let os_user = std::env::var("USER")
148
169
  .or_else(|_| std::env::var("USERNAME"))
@@ -207,17 +228,13 @@ fn emit_owner_bound_event(
207
228
  Ok(())
208
229
  }
209
230
 
210
- fn bind_provider_from_env_or_pane(command: &str, pane: Option<PaneInfo>) -> Provider {
231
+ fn bind_provider_from_env_or_pane(command: &str, pane: Option<PaneInfo>) -> Option<Provider> {
211
232
  let env_provider = std::env::var("TEAM_AGENT_LEADER_PROVIDER")
212
233
  .ok()
213
234
  .and_then(|raw| super::helpers::parse_provider(&raw));
214
235
  env_provider
215
236
  .or_else(|| pane.as_ref().and_then(super::attribute_pane_provider))
216
237
  .or_else(|| super::attribute_command_provider(command))
217
- // E11 层2:未知命令不再静默默认 codex(会误绑任意 provider + 喂错分类器)。
218
- // 无法识别时回落 Codex 仅作最末兜底,且该路径已被统一归因入口的显式 None 收窄
219
- // (调用方理应只在已知 leader 命令上 bind);保留以不改 fn 签名/上游 panic 面。
220
- .unwrap_or(Provider::Codex)
221
238
  }
222
239
 
223
240
  pub(crate) fn strict_owner_bind_provider(
@@ -352,13 +369,8 @@ mod e11_provider_bind_tests {
352
369
  }
353
370
 
354
371
  #[test]
355
- fn strict_provider_never_falls_back_from_unknown_explicit_or_command() {
356
- assert_eq!(
357
- strict_owner_bind_provider(Some("pi"), "codex"),
358
- Some(Provider::Pi)
359
- );
372
+ fn provider_independent_rejects_unknown_or_node_without_fallback() {
360
373
  assert_eq!(strict_owner_bind_provider(Some("unknown"), "codex"), None);
361
- assert_eq!(strict_owner_bind_provider(None, "pi"), Some(Provider::Pi));
362
374
  assert_eq!(strict_owner_bind_provider(None, "node"), None);
363
375
  }
364
376
 
@@ -394,3 +406,19 @@ mod e11_provider_bind_tests {
394
406
  assert_eq!(owner_bind_provider_wire("totally-unknown"), "");
395
407
  }
396
408
  }
409
+
410
+ #[cfg(test)]
411
+ mod claim_pi_carrier_tests {
412
+ use super::*;
413
+
414
+ #[test]
415
+ fn strict_provider_never_falls_back_from_unknown_explicit_or_command() {
416
+ assert_eq!(
417
+ strict_owner_bind_provider(Some("pi"), "codex"),
418
+ Some(Provider::Pi)
419
+ );
420
+ assert_eq!(strict_owner_bind_provider(Some("unknown"), "codex"), None);
421
+ assert_eq!(strict_owner_bind_provider(None, "pi"), Some(Provider::Pi));
422
+ assert_eq!(strict_owner_bind_provider(None, "node"), None);
423
+ }
424
+ }
@@ -204,6 +204,71 @@ fn try_readopt_refuses_when_different_live_owner_uuid_mismatches() {
204
204
  assert!(state.get("team_owner").is_none());
205
205
  }
206
206
 
207
+ #[test]
208
+ fn try_readopt_inactive_bash_wrapper_with_matching_owner_uuid() {
209
+ let ws = ws("readopt-bash-ok");
210
+ let mut state = json!({"session_name": "sess"});
211
+ let uuid = "c1e3f7a7aeaed95491667b4d9c3ce6fb";
212
+ let mut recv = receiver("%0", uuid, 1);
213
+ let owner = owner("%0", uuid, 1);
214
+ let event_log = crate::event_log::EventLog::new(&ws);
215
+ let pane = json!({
216
+ "pane_id": "%0",
217
+ "session_name": "sess",
218
+ "window_name": "pi",
219
+ "pane_current_command": "bash",
220
+ "pane_current_path": ws.join("sub").to_string_lossy(),
221
+ "active": false,
222
+ "leader_session_uuid": uuid,
223
+ "leader_env": {
224
+ "TEAM_AGENT_LEADER_SESSION_UUID": uuid,
225
+ "TEAM_AGENT_WORKSPACE": ws.to_string_lossy(),
226
+ "TEAM_AGENT_TEAM_ID": "sess"
227
+ }
228
+ });
229
+ let got = try_readopt_leader_pane(
230
+ &ws,
231
+ &mut state,
232
+ &mut recv,
233
+ &pane,
234
+ &json!({"targets": [pane.clone()]}),
235
+ Some(&owner),
236
+ Provider::Codex,
237
+ LeaseSource::Manual,
238
+ &event_log,
239
+ )
240
+ .unwrap();
241
+ assert!(got.is_some(), "matching inactive wrapper must readopt");
242
+ }
243
+
244
+ #[test]
245
+ fn try_readopt_bash_without_recorded_owner_uuid_refuses() {
246
+ let ws = ws("readopt-bash-no-id");
247
+ let mut state = json!({});
248
+ let mut recv = receiver("%0", "c1e3f7a7aeaed95491667b4d9c3ce6fb", 1);
249
+ let event_log = crate::event_log::EventLog::new(&ws);
250
+ let pane = json!({
251
+ "pane_id": "%0",
252
+ "session_name": "sess",
253
+ "pane_current_command": "bash",
254
+ "pane_current_path": ws.join("sub").to_string_lossy(),
255
+ "active": false
256
+ });
257
+ let got = try_readopt_leader_pane(
258
+ &ws,
259
+ &mut state,
260
+ &mut recv,
261
+ &pane,
262
+ &json!({"targets": [pane.clone()]}),
263
+ None,
264
+ Provider::Codex,
265
+ LeaseSource::Manual,
266
+ &event_log,
267
+ )
268
+ .unwrap();
269
+ assert!(got.is_none());
270
+ }
271
+
207
272
  #[test]
208
273
  fn rediscover_updates_unique_env_triple_match() {
209
274
  let ws = ws("rediscover");
@@ -42,7 +42,12 @@ pub fn try_readopt_leader_pane(
42
42
  )?;
43
43
  return Ok(None);
44
44
  };
45
- if !readopt_candidate_is_usable_leader(workspace, &candidate) {
45
+ if !readopt_candidate_is_usable_leader(
46
+ workspace,
47
+ &candidate,
48
+ owner_record,
49
+ receiver_provider,
50
+ ) {
46
51
  event_log.write(
47
52
  LeaderEvent::ReceiverRebindRequired.name(),
48
53
  json!({"reason": "leader_pane_unusable"}),
@@ -598,14 +603,39 @@ fn target_iter(targets: &Value) -> Vec<LeaderTarget> {
598
603
  Vec::new()
599
604
  }
600
605
 
601
- fn readopt_candidate_is_usable_leader(workspace: &Path, target: &LeaderTarget) -> bool {
602
- if !target.active || target.provider.is_none() {
603
- return false;
604
- }
605
- target
606
+ fn readopt_candidate_is_usable_leader(
607
+ workspace: &Path,
608
+ target: &LeaderTarget,
609
+ owner_record: Option<&TeamOwner>,
610
+ receiver_provider: Provider,
611
+ ) -> bool {
612
+ if !target
606
613
  .current_path
607
614
  .as_deref()
608
615
  .is_some_and(|path| path_in_workspace(path, workspace))
616
+ {
617
+ return false;
618
+ }
619
+ if target.provider.is_some() {
620
+ return true;
621
+ }
622
+ let Some(owner) = owner_record else {
623
+ return false;
624
+ };
625
+ if owner.pane_id.as_str() != target.pane_id.as_str() {
626
+ return false;
627
+ }
628
+ if owner.provider != receiver_provider {
629
+ return false;
630
+ }
631
+ match (
632
+ owner.leader_session_uuid.as_ref(),
633
+ target.leader_session_uuid.as_ref(),
634
+ ) {
635
+ (Some(recorded), Some(live)) => recorded == live,
636
+ (Some(_), None) => true,
637
+ (None, _) => false,
638
+ }
609
639
  }
610
640
 
611
641
  fn rediscover_candidate_is_usable_leader(target: &LeaderTarget) -> bool {
@@ -1859,6 +1859,17 @@ fn persist_managed_leader_binding(
1859
1859
  &serde_json::json!(socket),
1860
1860
  );
1861
1861
  let bound_panes = merge_bound_panes(existing_recv.as_ref(), new_binding);
1862
+ let existing_owner = state
1863
+ .get("teams")
1864
+ .and_then(|teams| teams.get(&team_key))
1865
+ .and_then(|team| team.get("team_owner"))
1866
+ .cloned();
1867
+ let keep_field = |existing: Option<&serde_json::Value>, key: &str, fallback: serde_json::Value| {
1868
+ existing
1869
+ .and_then(|value| value.get(key))
1870
+ .cloned()
1871
+ .unwrap_or(fallback)
1872
+ };
1862
1873
  let (scalar_pane, scalar_session, scalar_window, scalar_attached_at) =
1863
1874
  match existing_recv.as_ref() {
1864
1875
  Some(recv) if keep_existing_scalar => (
@@ -1867,20 +1878,41 @@ fn persist_managed_leader_binding(
1867
1878
  .unwrap_or(pane.as_str())
1868
1879
  .to_string(),
1869
1880
  recv.get("session_name")
1870
- .and_then(serde_json::Value::as_str)
1871
- .unwrap_or(session.as_str())
1872
- .to_string(),
1881
+ .cloned()
1882
+ .unwrap_or_else(|| serde_json::json!(session)),
1873
1883
  recv.get("window_name")
1874
- .and_then(serde_json::Value::as_str)
1875
- .unwrap_or(window.as_str())
1876
- .to_string(),
1884
+ .cloned()
1885
+ .unwrap_or_else(|| serde_json::json!(window)),
1877
1886
  recv.get("attached_at")
1878
1887
  .and_then(serde_json::Value::as_str)
1879
1888
  .unwrap_or(now.as_str())
1880
1889
  .to_string(),
1881
1890
  ),
1882
- _ => (pane.clone(), session.clone(), window.clone(), now.clone()),
1891
+ _ => (
1892
+ pane.clone(),
1893
+ serde_json::json!(session),
1894
+ serde_json::json!(window),
1895
+ now.clone(),
1896
+ ),
1883
1897
  };
1898
+ let leader_session_uuid = if keep_existing_scalar {
1899
+ keep_field(
1900
+ existing_recv.as_ref(),
1901
+ "leader_session_uuid",
1902
+ serde_json::json!(identity.leader_session_uuid),
1903
+ )
1904
+ } else {
1905
+ serde_json::json!(identity.leader_session_uuid)
1906
+ };
1907
+ let discovery = if keep_existing_scalar {
1908
+ keep_field(
1909
+ existing_recv.as_ref(),
1910
+ "discovery",
1911
+ serde_json::json!("managed_launcher"),
1912
+ )
1913
+ } else {
1914
+ serde_json::json!("managed_launcher")
1915
+ };
1884
1916
  let receiver = serde_json::json!({
1885
1917
  "mode": "direct_tmux",
1886
1918
  "status": "attached",
@@ -1890,20 +1922,43 @@ fn persist_managed_leader_binding(
1890
1922
  "session_name": scalar_session,
1891
1923
  "window_name": scalar_window,
1892
1924
  "tmux_socket": socket,
1893
- "leader_session_uuid": identity.leader_session_uuid,
1925
+ "leader_session_uuid": leader_session_uuid.clone(),
1894
1926
  "owner_epoch": owner_epoch,
1895
1927
  "attached_at": scalar_attached_at,
1896
- "discovery": "managed_launcher",
1928
+ "discovery": discovery,
1897
1929
  "bound_panes": bound_panes,
1898
1930
  });
1931
+ let owner_uuid = if keep_existing_scalar {
1932
+ keep_field(
1933
+ existing_owner.as_ref(),
1934
+ "leader_session_uuid",
1935
+ leader_session_uuid.clone(),
1936
+ )
1937
+ } else {
1938
+ leader_session_uuid.clone()
1939
+ };
1940
+ let claimed_via = if keep_existing_scalar {
1941
+ keep_field(
1942
+ existing_owner.as_ref(),
1943
+ "claimed_via",
1944
+ serde_json::json!("claim-leader"),
1945
+ )
1946
+ } else {
1947
+ serde_json::json!("claim-leader")
1948
+ };
1949
+ let claimed_at = if keep_existing_scalar {
1950
+ keep_field(existing_owner.as_ref(), "claimed_at", serde_json::json!(now))
1951
+ } else {
1952
+ serde_json::json!(now)
1953
+ };
1899
1954
  let owner = serde_json::json!({
1900
1955
  "pane_id": scalar_pane,
1901
1956
  "provider": provider.clone(),
1902
1957
  "machine_fingerprint": identity.machine_fingerprint,
1903
- "leader_session_uuid": identity.leader_session_uuid,
1958
+ "leader_session_uuid": owner_uuid,
1904
1959
  "owner_epoch": owner_epoch,
1905
- "claimed_at": now,
1906
- "claimed_via": "claim-leader",
1960
+ "claimed_at": claimed_at,
1961
+ "claimed_via": claimed_via,
1907
1962
  "os_user": identity.os_user,
1908
1963
  });
1909
1964
  if let Some(obj) = state.as_object_mut() {
@@ -3881,18 +3936,22 @@ mod tests {
3881
3936
  }
3882
3937
 
3883
3938
  fn persist_test_identity(workspace: &Path) -> LeaderIdentity {
3939
+ persist_test_identity_named(workspace, "tester")
3940
+ }
3941
+
3942
+ fn persist_test_identity_named(workspace: &Path, os_user: &str) -> LeaderIdentity {
3884
3943
  LeaderIdentity {
3885
3944
  leader_session_uuid: LeaderSessionUuid::derive(
3886
3945
  "fp",
3887
3946
  &workspace.to_string_lossy(),
3888
- "tester",
3947
+ os_user,
3889
3948
  "current",
3890
3949
  )
3891
3950
  .unwrap(),
3892
3951
  leader_session_uuid_source: LeaderSessionUuidSource::Derived,
3893
3952
  machine_fingerprint: "fp".to_string(),
3894
3953
  workspace_abspath: workspace.to_path_buf(),
3895
- os_user: "tester".to_string(),
3954
+ os_user: os_user.to_string(),
3896
3955
  team_id: TeamKey::new("current"),
3897
3956
  }
3898
3957
  }
@@ -3995,6 +4054,84 @@ mod tests {
3995
4054
  let _ = std::fs::remove_dir_all(&workspace);
3996
4055
  }
3997
4056
 
4057
+ #[test]
4058
+ fn multi_leader_persist_keeps_first_owner_uuid() {
4059
+ let workspace = std::env::temp_dir().join(format!(
4060
+ "ta-multi-leader-uuid-{}",
4061
+ std::process::id()
4062
+ ));
4063
+ std::fs::create_dir_all(workspace.join(".team").join("runtime")).unwrap();
4064
+ let original_uuid = "b2b230cfd645e78cf49e8147ba32ff3b";
4065
+ let state = serde_json::json!({
4066
+ "active_team_key": "current",
4067
+ "teams": {
4068
+ "current": {
4069
+ "leader_receiver": {
4070
+ "status": "attached",
4071
+ "pane_id": "%0",
4072
+ "discovery": "env_pane",
4073
+ "leader_session_uuid": original_uuid,
4074
+ "owner_epoch": 1,
4075
+ "attached_at": "2026-09-09T17:08:31.965311+00:00",
4076
+ "tmux_socket": "/private/tmp/tmux-501/ta-33bddc6e7687",
4077
+ "session_name": null,
4078
+ "provider": "codex"
4079
+ },
4080
+ "team_owner": {
4081
+ "pane_id": "%0",
4082
+ "leader_session_uuid": original_uuid,
4083
+ "owner_epoch": 1,
4084
+ "claimed_via": "quick-start"
4085
+ }
4086
+ }
4087
+ }
4088
+ });
4089
+ std::fs::write(
4090
+ crate::state::persist::runtime_state_path(&workspace),
4091
+ serde_json::to_vec_pretty(&state).unwrap(),
4092
+ )
4093
+ .unwrap();
4094
+ let mut plan = persist_test_plan(&workspace);
4095
+ plan.identity = Some(persist_test_identity_named(&workspace, "second-launcher"));
4096
+ let second_uuid = plan
4097
+ .identity
4098
+ .as_ref()
4099
+ .unwrap()
4100
+ .leader_session_uuid
4101
+ .as_str()
4102
+ .to_string();
4103
+ assert_ne!(second_uuid, original_uuid);
4104
+ let spawned = SpawnResult {
4105
+ pane_id: PaneId::new("%2"),
4106
+ session: SessionName::new(
4107
+ "team-agent-leader-codex-team-agent-live-0578-r4-19450903",
4108
+ ),
4109
+ window: WindowName::new("codex-second"),
4110
+ child_pid: Some(2),
4111
+ };
4112
+ persist_managed_leader_binding(&plan, &workspace, &spawned).expect("second persist");
4113
+ let loaded = crate::state::persist::load_runtime_state(&workspace).unwrap();
4114
+ let recv = &loaded["teams"]["current"]["leader_receiver"];
4115
+ let owner = &loaded["teams"]["current"]["team_owner"];
4116
+ assert_eq!(recv["pane_id"], serde_json::json!("%0"));
4117
+ assert_eq!(recv["leader_session_uuid"], serde_json::json!(original_uuid));
4118
+ assert_eq!(recv["discovery"], serde_json::json!("env_pane"));
4119
+ assert_eq!(recv["session_name"], serde_json::Value::Null);
4120
+ assert_eq!(recv["owner_epoch"], serde_json::json!(1));
4121
+ assert_eq!(
4122
+ owner["leader_session_uuid"],
4123
+ serde_json::json!(original_uuid)
4124
+ );
4125
+ let panes = recv["bound_panes"].as_array().expect("bound_panes");
4126
+ let ids: Vec<&str> = panes
4127
+ .iter()
4128
+ .filter_map(|pane| pane.get("pane_id").and_then(serde_json::Value::as_str))
4129
+ .collect();
4130
+ assert!(ids.contains(&"%0"), "bound_panes missing first: {recv}");
4131
+ assert!(ids.contains(&"%2"), "bound_panes missing second: {recv}");
4132
+ let _ = std::fs::remove_dir_all(&workspace);
4133
+ }
4134
+
3998
4135
  #[test]
3999
4136
  fn multi_leader_clear_promotes_remaining() {
4000
4137
  let workspace =
@@ -1,5 +1,8 @@
1
1
  use super::*;
2
2
 
3
+ #[path = "../../../tests/support/hermetic.rs"]
4
+ mod hermetic;
5
+
3
6
  // =====================================================================
4
7
  // 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
5
8
  // =====================================================================
@@ -111,18 +114,21 @@ fn owner_bind_refused_event_name_is_owner_bind_refused() {
111
114
  // old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
112
115
  // unimplemented → RED。
113
116
  #[test]
117
+ #[serial_test::serial(env)]
114
118
  fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
115
119
  let _lock = ENV_LOCK.lock().unwrap();
116
- let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
117
- std::fs::create_dir_all(&ws).unwrap();
118
- let _env = EnvGuard::apply(&[
119
- ("TMUX_PANE", Some("%7")),
120
- ("TEAM_AGENT_MACHINE_FINGERPRINT", Some("fp")),
121
- ("TEAM_AGENT_LEADER_PROVIDER", Some("codex")),
122
- ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
123
- ]);
120
+ let hermetic = hermetic::HermeticTestEnv::enter("owner-bound-uuid");
121
+ let ws = hermetic.workspace("emit");
122
+ let _pane = hermetic.with_env("TMUX_PANE", "%7");
123
+ let _fingerprint = hermetic.with_env("TEAM_AGENT_MACHINE_FINGERPRINT", "fp");
124
+ let _provider = hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", "codex");
124
125
  let result = bind_owner_from_caller_pane(&ws, &TeamKey::new("default"), None).unwrap();
125
- assert!(result.ok, "caller pane should produce an owner binding");
126
+ assert!(
127
+ result.ok,
128
+ "caller pane must produce an owner binding; reason={:?} hint={:?}",
129
+ result.reason,
130
+ result.hint
131
+ );
126
132
  let owner = result.owner.as_ref().expect("successful bind has owner");
127
133
  let full = owner
128
134
  .leader_session_uuid