@team-agent/installer 0.5.73 → 0.5.75

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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +19 -13
  4. package/crates/team-agent/src/cli/emit.rs +6 -3
  5. package/crates/team-agent/src/cli/mod.rs +151 -0
  6. package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
  8. package/crates/team-agent/src/cli/types.rs +6 -0
  9. package/crates/team-agent/src/compiler.rs +14 -1
  10. package/crates/team-agent/src/layout/worker_env.rs +366 -0
  11. package/crates/team-agent/src/leader/lease.rs +334 -53
  12. package/crates/team-agent/src/leader/registry.rs +177 -108
  13. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +12 -1
  14. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +2272 -128
  15. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
  16. package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
  17. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
  18. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
  19. package/crates/team-agent/src/lifecycle/restart/common.rs +3 -11
  20. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
  21. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +31 -0
  22. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
  23. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
  24. package/crates/team-agent/src/messaging/delivery.rs +226 -37
  25. package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
  26. package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
  27. package/crates/team-agent/src/model/spec.rs +81 -7
  28. package/crates/team-agent/src/provider/adapters/pi.rs +14 -0
  29. package/crates/team-agent/src/state/persist.rs +433 -4
  30. package/crates/team-agent/src/state/repository.rs +45 -2
  31. package/crates/team-agent/src/tmux_backend/tests.rs +414 -7
  32. package/crates/team-agent/src/tmux_backend.rs +274 -26
  33. package/crates/team-agent/src/transport/test_support.rs +18 -3
  34. package/crates/team-agent/src/transport.rs +99 -0
  35. package/package.json +4 -4
  36. package/skills/team-agent/SKILL.md +3 -0
  37. package/skills/team-agent/command-coverage.json +21 -0
  38. package/skills/team-agent/references/team-agent-operator.md +9 -2
@@ -183,13 +183,23 @@ pub(crate) fn annotate_runtime_tmux_endpoint(
183
183
  transport: &dyn Transport,
184
184
  workspace: &Path,
185
185
  ) {
186
- let transport_endpoint = transport.tmux_endpoint();
187
- let endpoint = crate::tmux_backend::owning_tmux_endpoint_from_state(state)
188
- .map(|(endpoint, _source)| endpoint.to_string())
189
- .or_else(|| transport_endpoint.clone());
190
- let Some(endpoint) = endpoint else {
186
+ let Some(transport_endpoint) = transport.tmux_endpoint() else {
191
187
  return;
192
188
  };
189
+ let persisted_endpoint = ["tmux_endpoint", "tmux_socket"]
190
+ .into_iter()
191
+ .find_map(|key| {
192
+ state
193
+ .get(key)
194
+ .and_then(serde_json::Value::as_str)
195
+ .filter(|endpoint| !endpoint.is_empty())
196
+ });
197
+ // The worker endpoint is its own authority. A restart/add-agent spawn
198
+ // must not let an independently-bound leader receiver rewrite it.
199
+ if persisted_endpoint.is_some_and(|endpoint| endpoint != transport_endpoint.as_str()) {
200
+ return;
201
+ }
202
+ let endpoint = transport_endpoint.clone();
193
203
  let endpoint_for_state = if Path::new(&endpoint).is_absolute() || endpoint == "default" {
194
204
  endpoint.clone()
195
205
  } else if endpoint == crate::tmux_backend::socket_name_for_workspace(workspace) {
@@ -212,7 +222,7 @@ pub(crate) fn annotate_runtime_tmux_endpoint(
212
222
  .cloned()
213
223
  .unwrap_or(serde_json::Value::Null),
214
224
  );
215
- if transport_endpoint.as_deref() == Some(endpoint.as_str()) {
225
+ if transport_endpoint == endpoint {
216
226
  if let Some(source) = selected_tmux_socket_source(transport, workspace) {
217
227
  obj.insert("tmux_socket_source".to_string(), serde_json::json!(source));
218
228
  }
@@ -277,6 +277,10 @@ pub(super) fn spawn_agents(
277
277
  provider,
278
278
  ),
279
279
  );
280
+ // The provider is the final typed value after profile overlays. Keep it
281
+ // separate from inherited LEADER_* identity and let the tmux invocation
282
+ // boundary add the actual pane/socket only for the provider process.
283
+ crate::layout::worker_env::inject_current_caller_provider(&mut env, provider);
280
284
  // BUG / C-1-2 / C-6-1 cr verdict — Copilot system_prompt 走 spawn env overlay +
281
285
  // per-worker AGENTS.md(B2 灵魂件降级):写
282
286
  // <workspace>/.team/runtime/copilot-instructions/<agent_id>/AGENTS.md
@@ -51,6 +51,10 @@
51
51
  //! sites can reference the phases by name. The phase fns themselves
52
52
  //! remain in launch.rs until the next batch of relocations.
53
53
 
54
+ #[cfg(test)]
55
+ #[path = "../../../tests/support/hermetic.rs"]
56
+ mod hermetic;
57
+
54
58
  use std::collections::{BTreeMap, BTreeSet};
55
59
  use std::path::{Path, PathBuf};
56
60
  use std::process::Command;
@@ -66,16 +70,14 @@ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
66
70
  use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
67
71
 
68
72
  use super::identity::spec_display_backend;
69
- use super::leader_context::{
70
- attributed_provider_for_pane_across_tmux_sockets, caller_provider_for_seed_with_lookup,
71
- seed_unbound_launched_owner,
72
- };
73
+ use super::leader_context::{caller_provider_for_seed_with_lookup, seed_unbound_launched_owner};
73
74
  use super::worker_env::spawn_timestamp;
74
75
 
75
76
  /// ---
76
77
  /// purpose: 由 spec 生成首份 runtime state 树
77
78
  /// params:
78
79
  /// team_key: 本团队的 runtime 键
80
+ /// scoped_endpoint: 本次起队已选中的 transport tmux endpoint,与 fresh bind 同源
79
81
  /// returns: 含 spec_path、workspace、team_dir、session_name、leader、agents、tasks 与 display_backend 的 state;随后按环境种入 launched owner,种不到则种一份 unbound owner
80
82
  /// ---
81
83
  pub(super) fn initial_runtime_state(
@@ -84,6 +86,7 @@ pub(super) fn initial_runtime_state(
84
86
  workspace: &Path,
85
87
  team_dir: &Path,
86
88
  team_key: &str,
89
+ scoped_endpoint: Option<&str>,
87
90
  ) -> serde_json::Value {
88
91
  let mut agents = serde_json::Map::new();
89
92
  for agent in spec_agent_values(spec) {
@@ -144,7 +147,7 @@ pub(super) fn initial_runtime_state(
144
147
  );
145
148
  state.insert("is_external_leader".to_string(), serde_json::json!(false));
146
149
  let mut state = serde_json::Value::Object(state);
147
- if !seed_launched_owner_from_env(&mut state) {
150
+ if !seed_launched_owner_from_env(&mut state, scoped_endpoint) {
148
151
  let team_id = crate::state::projection::team_state_key(&state);
149
152
  seed_unbound_launched_owner(&mut state, &team_id);
150
153
  }
@@ -157,9 +160,12 @@ pub(super) fn initial_runtime_state(
157
160
  /// state: 待改写的 state,成功时写入 owner 与 leader receiver
158
161
  /// returns: true 表示已种入,取不到 caller 身份或无 pane 时为 false
159
162
  /// ---
160
- pub(super) fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
163
+ pub(super) fn seed_launched_owner_from_env(
164
+ state: &mut serde_json::Value,
165
+ scoped_endpoint: Option<&str>,
166
+ ) -> bool {
161
167
  let team_id = crate::state::projection::team_state_key(state);
162
- let Ok(caller) = crate::state::identity::caller_identity_from_env(
168
+ let Ok(mut caller) = crate::state::identity::caller_identity_from_env(
163
169
  Some(state),
164
170
  &crate::state::identity::SystemEnv,
165
171
  Some(&team_id),
@@ -167,11 +173,27 @@ pub(super) fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> boo
167
173
  ) else {
168
174
  return false;
169
175
  };
170
- seed_launched_owner_from_caller_with_provider_lookup(
171
- state,
172
- caller,
173
- attributed_provider_for_pane_across_tmux_sockets,
174
- )
176
+ let current_pane = std::env::var("TMUX_PANE")
177
+ .ok()
178
+ .filter(|pane| !pane.is_empty());
179
+ let current_endpoint = crate::tmux_backend::socket_name_from_tmux_env();
180
+ match crate::layout::worker_env::caller_provider_resolution(
181
+ current_pane.as_deref(),
182
+ current_endpoint.as_deref(),
183
+ scoped_endpoint,
184
+ ) {
185
+ crate::layout::worker_env::CallerProviderResolution::Invalid => return false,
186
+ crate::layout::worker_env::CallerProviderResolution::Valid(provider) => {
187
+ caller.provider = crate::provider::wire::provider_wire(provider).to_string();
188
+ }
189
+ crate::layout::worker_env::CallerProviderResolution::Absent => {}
190
+ }
191
+ // Absent keeps the existing explicit/direct pane path. Do not unbind a
192
+ // caller TMUX_PANE just because launcher context and LEADER_PROVIDER are
193
+ // both missing; that is the external-leader same-id-across-sockets case.
194
+ // Invalid already returned. Do not replace a missing provider with broad
195
+ // pane/process attribution.
196
+ seed_launched_owner_from_caller_with_provider_lookup(state, caller, |_| None)
175
197
  }
176
198
 
177
199
  /// ---
@@ -619,9 +641,13 @@ pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
619
641
 
620
642
  #[cfg(test)]
621
643
  mod tests {
622
- #![allow(clippy::unwrap_used)]
644
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
623
645
  use super::*;
646
+ use crate::layout::worker_env::{CALLER_ENDPOINT_ENV, CALLER_PANE_ENV, CALLER_PROVIDER_ENV};
624
647
  use crate::model::enums::Provider;
648
+ use serde_json::json;
649
+
650
+ use super::hermetic::HermeticTestEnv;
625
651
 
626
652
  fn yaml_agent(bypass: bool) -> Value {
627
653
  crate::model::yaml::loads(&format!(
@@ -651,4 +677,103 @@ mod tests {
651
677
  assert!(!safety.enabled);
652
678
  assert_eq!(safety.source, DangerousApprovalSource::Disabled);
653
679
  }
680
+
681
+ fn seed_state() -> serde_json::Value {
682
+ json!({
683
+ "team_key": "verify031",
684
+ "workspace": "/tmp/ws",
685
+ "session_name": "team-verify031",
686
+ "agents": {}
687
+ })
688
+ }
689
+
690
+ fn owner_pane(state: &serde_json::Value) -> Option<&str> {
691
+ state
692
+ .pointer("/teams/verify031/team_owner/pane_id")
693
+ .and_then(serde_json::Value::as_str)
694
+ }
695
+
696
+ fn owner_provider(state: &serde_json::Value) -> Option<&str> {
697
+ state
698
+ .pointer("/teams/verify031/team_owner/provider")
699
+ .and_then(serde_json::Value::as_str)
700
+ }
701
+
702
+ fn receiver_pane(state: &serde_json::Value) -> Option<&str> {
703
+ state
704
+ .pointer("/teams/verify031/leader_receiver/pane_id")
705
+ .and_then(serde_json::Value::as_str)
706
+ }
707
+
708
+ #[test]
709
+ #[serial_test::serial(env)]
710
+ fn seed_absent_caller_keeps_direct_tmux_pane_without_unbinding() {
711
+ let hermetic = HermeticTestEnv::enter("seed-absent-direct-pane");
712
+ let _tmux = hermetic.with_env("TMUX", "/tmp/default-tmux-socket,123,0");
713
+ let _pane = hermetic.with_env("TMUX_PANE", "%0");
714
+ let mut state = seed_state();
715
+ assert!(
716
+ seed_launched_owner_from_env(&mut state, Some("/tmp/product-team.sock")),
717
+ "Absent caller context must still seed the caller's TMUX_PANE; state={state}"
718
+ );
719
+ assert_eq!(owner_pane(&state), Some("%0"));
720
+ assert_eq!(receiver_pane(&state), Some("%0"));
721
+ assert!(
722
+ owner_provider(&state).is_none(),
723
+ "direct pane seed must not invent a provider; state={state}"
724
+ );
725
+ }
726
+
727
+ #[test]
728
+ #[serial_test::serial(env)]
729
+ fn seed_valid_caller_uses_typed_provider_and_same_scoped_endpoint() {
730
+ let hermetic = HermeticTestEnv::enter("seed-valid-scoped");
731
+ let _tmux = hermetic.with_env("TMUX", "/tmp/tmux.sock,1,0");
732
+ let _pane = hermetic.with_env("TMUX_PANE", "%1");
733
+ let _provider = hermetic.with_env(CALLER_PROVIDER_ENV, "pi");
734
+ let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
735
+ let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
736
+ let mut state = seed_state();
737
+ assert!(seed_launched_owner_from_env(
738
+ &mut state,
739
+ Some("/tmp/tmux.sock")
740
+ ));
741
+ assert_eq!(owner_pane(&state), Some("%1"));
742
+ assert_eq!(owner_provider(&state), Some("pi"));
743
+ assert_eq!(receiver_pane(&state), Some("%1"));
744
+ }
745
+
746
+ #[test]
747
+ #[serial_test::serial(env)]
748
+ fn seed_invalid_caller_does_not_fall_back_to_direct_pane() {
749
+ let hermetic = HermeticTestEnv::enter("seed-invalid-scoped");
750
+ let _tmux = hermetic.with_env("TMUX", "/tmp/tmux.sock,1,0");
751
+ let _pane = hermetic.with_env("TMUX_PANE", "%1");
752
+ let _provider = hermetic.with_env(CALLER_PROVIDER_ENV, "pi");
753
+ let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
754
+ let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
755
+ let mut state = seed_state();
756
+ assert!(
757
+ !seed_launched_owner_from_env(&mut state, Some("/tmp/product.sock")),
758
+ "scoped endpoint mismatch must fail closed; state={state}"
759
+ );
760
+ assert!(owner_pane(&state).is_none(), "state={state}");
761
+ assert!(receiver_pane(&state).is_none(), "state={state}");
762
+ }
763
+
764
+ #[test]
765
+ #[serial_test::serial(env)]
766
+ fn seed_absent_keeps_explicit_leader_provider() {
767
+ let hermetic = HermeticTestEnv::enter("seed-absent-explicit");
768
+ let _tmux = hermetic.with_env("TMUX", "/tmp/tmux.sock,1,0");
769
+ let _pane = hermetic.with_env("TMUX_PANE", "%3");
770
+ let _leader = hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", "pi");
771
+ let mut state = seed_state();
772
+ assert!(seed_launched_owner_from_env(
773
+ &mut state,
774
+ Some("/tmp/tmux.sock")
775
+ ));
776
+ assert_eq!(owner_pane(&state), Some("%3"));
777
+ assert_eq!(owner_provider(&state), Some("pi"));
778
+ }
654
779
  }
@@ -577,15 +577,17 @@ pub(super) fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]
577
577
  }
578
578
 
579
579
  /// ---
580
- /// purpose: 给出 worker 侧使用的 tmux socket 名
581
- /// returns: transport 是 tmux 后端时按 workspace 推出 socket 名,否则 None
580
+ /// purpose: 给出 worker 侧实际使用的 tmux endpoint
581
+ /// returns: transport 是 tmux 后端时优先返回 transport endpoint;缺失时按 workspace 推出 socket 名,否则 None
582
582
  /// ---
583
583
  pub(super) fn launched_worker_tmux_socket(
584
584
  transport: &dyn Transport,
585
585
  workspace: &Path,
586
586
  ) -> Option<String> {
587
587
  if matches!(transport.kind(), crate::transport::BackendKind::Tmux) {
588
- Some(crate::tmux_backend::socket_name_for_workspace(workspace))
588
+ transport
589
+ .tmux_endpoint()
590
+ .or_else(|| Some(crate::tmux_backend::socket_name_for_workspace(workspace)))
589
591
  } else {
590
592
  None
591
593
  }
@@ -523,6 +523,9 @@ pub(super) fn spawn_agent_window(
523
523
  provider,
524
524
  ),
525
525
  );
526
+ // Keep the launcher-known typed provider independent from inherited
527
+ // LEADER_* identity. The tmux wrapper binds pane/socket at invocation time.
528
+ crate::layout::worker_env::inject_current_caller_provider(&mut env, provider);
526
529
 
527
530
  // 0.4.6 Stage 2: write actual spawn plan event BEFORE invoking the
528
531
  // transport spawn. Mirrors `launch.rs:359-380` (the reference impl)
@@ -976,14 +979,6 @@ pub(super) fn lifecycle_worker_tmux_backend_selection_for_state(
976
979
  )));
977
980
  }
978
981
  }
979
- if let Some((endpoint, source)) = crate::tmux_backend::owning_tmux_endpoint_from_state(state) {
980
- let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint);
981
- return Ok(crate::tmux_backend::RuntimeTmuxBackendSelection {
982
- tmux_endpoint_used: backend.tmux_endpoint(),
983
- backend,
984
- tmux_endpoint_source: source,
985
- });
986
- }
987
982
  Ok(
988
983
  crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(
989
984
  run_workspace,
@@ -1000,9 +995,6 @@ pub(super) fn lifecycle_worker_tmux_backend_for_state(
1000
995
  run_workspace: &Path,
1001
996
  state: &serde_json::Value,
1002
997
  ) -> crate::tmux_backend::TmuxBackend {
1003
- if let Some((endpoint, _source)) = crate::tmux_backend::owning_tmux_endpoint_from_state(state) {
1004
- return crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint);
1005
- }
1006
998
  crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(run_workspace, Some(state))
1007
999
  .backend
1008
1000
  }
@@ -3496,6 +3496,66 @@ fn add_agent_resolves_to_persisted_endpoint_socket_not_workspace_hash() {
3496
3496
  );
3497
3497
  }
3498
3498
 
3499
+ /// **Regression guard**: lifecycle worker resolution must ignore the leader
3500
+ /// receiver endpoint. The receiver may live on the caller's default socket
3501
+ /// while workers/coordinator remain on their persisted private socket.
3502
+ #[test]
3503
+ fn lifecycle_worker_resolver_prefers_persisted_worker_over_leader_receiver() {
3504
+ let team = temp_ws().join("addteam-split-leader-worker-endpoints");
3505
+ std::fs::create_dir_all(&team).unwrap();
3506
+ let worker_endpoint = "/private/tmp/tmux-501/ta-worker";
3507
+ let leader_endpoint = "/private/tmp/tmux-501/default";
3508
+ crate::state::persist::save_runtime_state(
3509
+ &team,
3510
+ &json!({
3511
+ "session_name": "team-split",
3512
+ "tmux_endpoint": worker_endpoint,
3513
+ "tmux_socket": worker_endpoint,
3514
+ "leader_receiver": {
3515
+ "pane_id": "%169",
3516
+ "tmux_socket": leader_endpoint,
3517
+ "status": "attached"
3518
+ },
3519
+ "agents": {}
3520
+ }),
3521
+ )
3522
+ .unwrap();
3523
+
3524
+ let resolved = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
3525
+ &team, None,
3526
+ )
3527
+ .expect("resolver must succeed when state has worker and receiver endpoints");
3528
+ let endpoint = <crate::tmux_backend::TmuxBackend as crate::transport::Transport>::tmux_endpoint(
3529
+ &resolved,
3530
+ );
3531
+ assert_eq!(
3532
+ endpoint.as_deref(),
3533
+ Some(worker_endpoint),
3534
+ "worker lifecycle operations must stay on persisted worker endpoint, not leader receiver"
3535
+ );
3536
+ }
3537
+
3538
+ /// **Regression guard**: annotating a worker spawn must not overwrite a
3539
+ /// persisted worker endpoint with the independent leader receiver endpoint.
3540
+ #[test]
3541
+ fn annotate_runtime_tmux_endpoint_does_not_follow_leader_receiver() {
3542
+ let worker_endpoint = "/private/tmp/tmux-501/ta-worker";
3543
+ let leader_endpoint = "/private/tmp/tmux-501/default";
3544
+ let mut state = json!({
3545
+ "tmux_endpoint": worker_endpoint,
3546
+ "tmux_socket": worker_endpoint,
3547
+ "leader_receiver": {"tmux_socket": leader_endpoint}
3548
+ });
3549
+ let transport = OfflineTransport::new().with_tmux_endpoint(worker_endpoint);
3550
+ crate::lifecycle::launch::annotate_runtime_tmux_endpoint(
3551
+ &mut state,
3552
+ &transport,
3553
+ std::path::Path::new("/tmp/annotate-split"),
3554
+ );
3555
+ assert_eq!(state["tmux_endpoint"], json!(worker_endpoint));
3556
+ assert_eq!(state["tmux_socket"], json!(worker_endpoint));
3557
+ }
3558
+
3499
3559
  /// **Regression guard**: cold workspace (no persisted endpoint) must safely
3500
3560
  /// fall back to `TmuxBackend::for_workspace(workspace)`. Encodes the
3501
3561
  /// first-agent / fresh-launch path so a future refactor doesn't accidentally
@@ -243,3 +243,34 @@ fn pi_role_tools_validate_team_capabilities_without_replacing_direct_pi_tools()
243
243
  "role tools validate Team Agent capabilities but must not replace direct Pi tools: {argv:?}"
244
244
  );
245
245
  }
246
+
247
+ #[test]
248
+ fn pi_role_rejects_unsupported_categories_during_compile() {
249
+ compile_pi_role(&valid_role_with("")).expect("supported Pi tools must compile");
250
+
251
+ for unsupported in ["git_diff", "network", "provider_builtin"] {
252
+ let role = valid_role_with("").replace(
253
+ " - execute_bash\n",
254
+ &format!(" - execute_bash\n - {unsupported}\n"),
255
+ );
256
+ let error = compile_error(compile_pi_role(&role)).to_string();
257
+ assert!(
258
+ error.contains(&format!("Pi does not support Team Agent tool category {unsupported:?}")),
259
+ "compile must reject {unsupported} before launch preparation: {error}"
260
+ );
261
+ assert!(
262
+ error.contains("remove it from the role's tools"),
263
+ "compile error must provide an actionable fix: {error}"
264
+ );
265
+ }
266
+
267
+ let alias = valid_role_with("").replace(
268
+ " - execute_bash\n",
269
+ " - execute_bash\n - \"@builtin\"\n",
270
+ );
271
+ let error = compile_error(compile_pi_role(&alias)).to_string();
272
+ assert!(
273
+ error.contains("Pi does not support Team Agent tool category \"provider_builtin\""),
274
+ "@builtin must use the same Pi capability contract: {error}"
275
+ );
276
+ }
@@ -320,26 +320,26 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
320
320
  .get("agents")
321
321
  .cloned()
322
322
  .expect("fixture agents");
323
- let owning_socket = "/private/tmp/tmux-501/ta-owning-team";
323
+ let receiver_socket = "/private/tmp/tmux-501/ta-leader-receiver";
324
324
  let owning_session = "team-agent-leader-pi-owning-team";
325
- let wrong_socket = "/private/tmp/tmux-501/ta-workspace-derived";
325
+ let worker_socket = "/private/tmp/tmux-501/ta-worker-persisted";
326
326
  crate::state::persist::save_runtime_state(
327
327
  &success_team,
328
328
  &json!({
329
329
  "active_team_key": "dynamic-success",
330
- "tmux_endpoint": wrong_socket,
331
- "tmux_socket": wrong_socket,
330
+ "tmux_endpoint": worker_socket,
331
+ "tmux_socket": worker_socket,
332
332
  "teams": {
333
333
  "dynamic-success": {
334
334
  "agents": success_agents,
335
335
  "team_dir": success_team,
336
336
  "session_name": null,
337
- "tmux_endpoint": wrong_socket,
338
- "tmux_socket": wrong_socket,
337
+ "tmux_endpoint": worker_socket,
338
+ "tmux_socket": worker_socket,
339
339
  "leader_receiver": {
340
340
  "status": "attached",
341
341
  "session_name": owning_session,
342
- "tmux_socket": owning_socket
342
+ "tmux_socket": receiver_socket
343
343
  }
344
344
  }
345
345
  }
@@ -351,15 +351,15 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
351
351
  &success_team,
352
352
  Some("dynamic-success"),
353
353
  )
354
- .expect("resolve selected owning transport");
354
+ .expect("resolve selected worker transport");
355
355
  assert_eq!(
356
356
  selected_transport.tmux_endpoint().as_deref(),
357
- Some(owning_socket),
358
- "nested selected-team receiver must beat stale top-level and team socket fields"
357
+ Some(worker_socket),
358
+ "lifecycle worker transport must use persisted worker endpoint, not receiver endpoint"
359
359
  );
360
360
  let mut annotation_state =
361
361
  crate::state::persist::load_runtime_state(&success_team).expect("load annotation fixture");
362
- let wrong_transport = OfflineTransport::new().with_tmux_endpoint(wrong_socket);
362
+ let wrong_transport = OfflineTransport::new().with_tmux_endpoint(worker_socket);
363
363
  crate::lifecycle::launch::annotate_runtime_tmux_endpoint(
364
364
  &mut annotation_state,
365
365
  &wrong_transport,
@@ -369,18 +369,18 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
369
369
  annotation_state
370
370
  .get("tmux_endpoint")
371
371
  .and_then(serde_json::Value::as_str),
372
- Some(owning_socket),
373
- "worker-derived annotation must not clobber the owning endpoint"
372
+ Some(worker_socket),
373
+ "worker-derived annotation must preserve the persisted worker endpoint"
374
374
  );
375
375
  assert_eq!(
376
376
  annotation_state
377
377
  .pointer("/teams/dynamic-success/leader_receiver/tmux_socket")
378
378
  .and_then(serde_json::Value::as_str),
379
- Some(owning_socket)
379
+ Some(receiver_socket)
380
380
  );
381
381
  let success_transport = OfflineTransport::new()
382
382
  .with_session_present(true)
383
- .with_tmux_endpoint(owning_socket);
383
+ .with_tmux_endpoint(worker_socket);
384
384
  crate::lifecycle::add_agent_with_transport(
385
385
  &success_team,
386
386
  &AgentId::new("mate"),
@@ -516,8 +516,8 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
516
516
  &live,
517
517
  &json!({
518
518
  "active_team_key": "current",
519
- "tmux_endpoint": wrong_socket,
520
- "tmux_socket": wrong_socket,
519
+ "tmux_endpoint": worker_socket,
520
+ "tmux_socket": worker_socket,
521
521
  "agents": {
522
522
  "implementer": {
523
523
  "status": "running",
@@ -532,6 +532,8 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
532
532
  "status": "alive",
533
533
  "team_dir": live,
534
534
  "session_name": null,
535
+ "tmux_endpoint": worker_socket,
536
+ "tmux_socket": worker_socket,
535
537
  "agents": {
536
538
  "implementer": {
537
539
  "status": "running",
@@ -655,9 +657,9 @@ int main(int argc, char **argv) {
655
657
 
656
658
  let preflight = run_file_h_command_with_deadline(
657
659
  std::process::Command::new(&shim)
658
- .args(["-S", live_socket, "has-session"])
660
+ .args(["-S", worker_socket, "has-session"])
659
661
  .env("TEAM_AGENT_PI_TMUX_LOG", shim_root.join("preflight.log"))
660
- .env("TEAM_AGENT_PI_OWNING_SOCKET", live_socket)
662
+ .env("TEAM_AGENT_PI_OWNING_SOCKET", worker_socket)
661
663
  .env("TEAM_AGENT_PI_OWNING_SESSION", live_session)
662
664
  .env("TEAM_AGENT_PI_RUN_WORKSPACE", &live),
663
665
  "File-H native fake has-session preflight",
@@ -678,7 +680,7 @@ int main(int argc, char **argv) {
678
680
  .expect("join native shim PATH");
679
681
  let _path = EnvVarGuard::set("PATH", joined_path);
680
682
  let _log = EnvVarGuard::set("TEAM_AGENT_PI_TMUX_LOG", &shim_log);
681
- let _socket = EnvVarGuard::set("TEAM_AGENT_PI_OWNING_SOCKET", live_socket);
683
+ let _socket = EnvVarGuard::set("TEAM_AGENT_PI_OWNING_SOCKET", worker_socket);
682
684
  let _session = EnvVarGuard::set("TEAM_AGENT_PI_OWNING_SESSION", live_session);
683
685
  let _workspace = EnvVarGuard::set("TEAM_AGENT_PI_RUN_WORKSPACE", &live);
684
686
 
@@ -689,15 +691,15 @@ int main(int argc, char **argv) {
689
691
  );
690
692
  let result = tools
691
693
  .add_agent("mate", "roles/mate.md")
692
- .expect("public MCP add must use the nested owning receiver");
694
+ .expect("public MCP add must use the persisted worker endpoint");
693
695
  assert_eq!(
694
696
  result.fields.get("ok").and_then(serde_json::Value::as_bool),
695
697
  Some(true)
696
698
  );
697
699
  let tmux_argv = std::fs::read_to_string(&shim_log).expect("read tmux shim log");
698
700
  assert!(
699
- tmux_argv.contains(&format!("-S {live_socket}")),
700
- "public add must address the selected owning socket: {tmux_argv}"
701
+ tmux_argv.contains(&format!("-S {worker_socket}")),
702
+ "public add must address the persisted worker socket: {tmux_argv}"
701
703
  );
702
704
  assert!(
703
705
  tmux_argv.contains(&format!(