@team-agent/installer 0.5.68 → 0.5.69

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 (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/diagnose.rs +74 -0
  4. package/crates/team-agent/src/cli/emit.rs +5 -4
  5. package/crates/team-agent/src/cli/leader.rs +1 -0
  6. package/crates/team-agent/src/cli/spec.rs +1 -0
  7. package/crates/team-agent/src/compiler.rs +62 -5
  8. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  9. package/crates/team-agent/src/layout/worker_window_helpers.rs +1 -0
  10. package/crates/team-agent/src/leader/start.rs +53 -12
  11. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +67 -15
  12. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +18 -0
  13. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +1 -0
  14. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +260 -44
  15. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  16. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +5 -1
  17. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +10 -59
  18. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  19. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
  20. package/crates/team-agent/src/lifecycle/launch/spawn.rs +70 -28
  21. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +1 -0
  22. package/crates/team-agent/src/lifecycle/launch.rs +10 -2
  23. package/crates/team-agent/src/lifecycle/profile_launch.rs +3 -2
  24. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -0
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +90 -160
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +146 -28
  27. package/crates/team-agent/src/lifecycle/restart/remove.rs +37 -1
  28. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  29. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +195 -18
  30. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +30 -12
  31. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  32. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +5 -2
  33. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +35 -48
  34. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  35. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  36. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  37. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  38. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  39. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  40. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  41. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  42. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -0
  43. package/crates/team-agent/src/lifecycle/tests.rs +7 -0
  44. package/crates/team-agent/src/lifecycle/worker_command_context.rs +61 -4
  45. package/crates/team-agent/src/messaging/delivery.rs +36 -12
  46. package/crates/team-agent/src/model/enums.rs +3 -1
  47. package/crates/team-agent/src/model/permissions.rs +1 -1
  48. package/crates/team-agent/src/provider/adapter.rs +29 -3
  49. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +5 -2
  50. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  51. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  52. package/crates/team-agent/src/provider/bypass_flags.rs +1 -0
  53. package/crates/team-agent/src/provider/classify.rs +2 -2
  54. package/crates/team-agent/src/provider/faults.rs +1 -1
  55. package/crates/team-agent/src/provider/session/context_fork.rs +2 -1
  56. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  57. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  58. package/crates/team-agent/src/provider/session_scan.rs +4 -0
  59. package/crates/team-agent/src/provider/tests/adapter.rs +80 -0
  60. package/crates/team-agent/src/provider/wire.rs +7 -1
  61. package/crates/team-agent/src/tmux_backend.rs +52 -0
  62. package/package.json +4 -4
  63. package/skills/team-agent/SKILL.md +2 -4
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.68"
584
+ version = "0.5.69"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.68"
12
+ version = "0.5.69"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -9,6 +9,74 @@ use crate::model::pane_authority_refusal::{
9
9
  use crate::provider::wire::{command_name, parse_provider, provider_wire};
10
10
  use crate::transport::Transport;
11
11
 
12
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
13
+ #[serde(rename_all = "snake_case")]
14
+ pub(crate) enum PiBackingStatus {
15
+ Pending,
16
+ Captured,
17
+ Missing,
18
+ }
19
+
20
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
21
+ #[serde(rename_all = "snake_case")]
22
+ pub(crate) enum PiMcpStatus {
23
+ Unavailable,
24
+ ConfiguredLazy,
25
+ RoundtripVerified,
26
+ }
27
+
28
+ pub(crate) struct PiDoctorInput {
29
+ pub executable_chain_verified: bool,
30
+ pub pi_version: Option<String>,
31
+ pub adapter_version: Option<String>,
32
+ pub catalog_sha256: Option<String>,
33
+ pub selected_model: Option<String>,
34
+ pub candidate_executable: Option<std::path::PathBuf>,
35
+ pub wrapper: Option<std::path::PathBuf>,
36
+ pub mcp_roundtrip_at: Option<String>,
37
+ pub backing: PiBackingStatus,
38
+ pub activity: crate::provider::TurnState,
39
+ pub activity_source: crate::provider::ClassifySource,
40
+ pub env_key_names: Vec<String>,
41
+ }
42
+
43
+ #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
44
+ pub(crate) struct PiDoctorFacts {
45
+ pub mcp: PiMcpStatus,
46
+ pub mcp_connected: bool,
47
+ pub backing: PiBackingStatus,
48
+ pub activity: crate::provider::TurnState,
49
+ pub activity_source: crate::provider::ClassifySource,
50
+ pub auth_hint: crate::provider::AuthHintStatus,
51
+ }
52
+
53
+ pub(crate) fn pi_doctor_facts(input: PiDoctorInput) -> PiDoctorFacts {
54
+ let mcp = if input.mcp_roundtrip_at.is_some() {
55
+ PiMcpStatus::RoundtripVerified
56
+ } else if input.wrapper.is_some() {
57
+ PiMcpStatus::ConfiguredLazy
58
+ } else {
59
+ PiMcpStatus::Unavailable
60
+ };
61
+ let _ = (
62
+ input.executable_chain_verified,
63
+ input.pi_version,
64
+ input.adapter_version,
65
+ input.catalog_sha256,
66
+ input.selected_model,
67
+ input.candidate_executable,
68
+ input.env_key_names,
69
+ );
70
+ PiDoctorFacts {
71
+ mcp,
72
+ mcp_connected: mcp == PiMcpStatus::RoundtripVerified,
73
+ backing: input.backing,
74
+ activity: input.activity,
75
+ activity_source: input.activity_source,
76
+ auth_hint: crate::provider::AuthHintStatus::Unknown,
77
+ }
78
+ }
79
+
12
80
  /// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B): classify a tmux
13
81
  /// transport error string into a diagnose issue id. When the underlying
14
82
  /// tmux subprocess stderr contains `server exited unexpectedly`, the
@@ -1424,6 +1492,11 @@ fn fake_agent_started(agent: &Value) -> bool {
1424
1492
  }
1425
1493
 
1426
1494
  fn agent_mcp_ready(agent: &Value) -> bool {
1495
+ if agent.get("provider").and_then(Value::as_str) == Some("pi") {
1496
+ // The generated wrapper proves configured-lazy only. No persisted
1497
+ // objective Pi MCP roundtrip receipt exists on the readiness path yet.
1498
+ return false;
1499
+ }
1427
1500
  agent
1428
1501
  .get("mcp_config")
1429
1502
  .and_then(Value::as_str)
@@ -1567,6 +1640,7 @@ pub(crate) fn provider_doctor_checks() -> Value {
1567
1640
  crate::provider::Provider::GeminiCli,
1568
1641
  crate::provider::Provider::Grok,
1569
1642
  crate::provider::Provider::CursorAgent,
1643
+ crate::provider::Provider::Pi,
1570
1644
  crate::provider::Provider::Fake,
1571
1645
  ] {
1572
1646
  let adapter = crate::provider::get_adapter(provider);
@@ -262,9 +262,10 @@ const DISPATCH_COMMANDS: &[&str] = &[
262
262
  const SPEC_ONLY_HELP_COMMANDS: &[&str] = &["start"];
263
263
  // Command grammar, not provider identity parsing: these are top-level CLI
264
264
  // passthrough verbs for starting a leader under a provider executable.
265
- const LEADER_PASSTHROUGH_COMMANDS: &[&str] = &["codex", "claude", "copilot", "grok", "cursor"];
265
+ const LEADER_PASSTHROUGH_COMMANDS: &[&str] =
266
+ &["codex", "claude", "copilot", "grok", "cursor", "pi"];
266
267
 
267
- fn is_leader_passthrough_command(command: &str) -> bool {
268
+ pub(crate) fn is_leader_passthrough_command(command: &str) -> bool {
268
269
  LEADER_PASSTHROUGH_COMMANDS.contains(&command)
269
270
  && matches!(
270
271
  command_spec(command).map(|spec| spec.kind),
@@ -285,7 +286,7 @@ fn is_known_subcommand(command: &str) -> bool {
285
286
  command_spec(command).is_some_and(|spec| spec.command_help)
286
287
  }
287
288
 
288
- fn default_help() -> String {
289
+ pub(crate) fn default_help() -> String {
289
290
  let mut out = String::from("usage: team-agent <command> [options]\n");
290
291
  append_help_section(
291
292
  &mut out,
@@ -310,7 +311,7 @@ fn default_help() -> String {
310
311
  "Guided recovery",
311
312
  &["claim-leader", "takeover", "attach-leader"],
312
313
  );
313
- out.push_str("\nProvider launchers:\n team-agent codex|claude|copilot|grok|cursor ...\n");
314
+ out.push_str("\nProvider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
314
315
  out.push_str("\nRun `team-agent <command> --help` for command flags.");
315
316
  out
316
317
  }
@@ -130,6 +130,7 @@ pub(crate) fn leader_passthrough_provider(
130
130
  "copilot" => Some(Provider::Copilot),
131
131
  "grok" => Some(Provider::Grok),
132
132
  "cursor" => Some(Provider::CursorAgent),
133
+ "pi" => Some(Provider::Pi),
133
134
  _ => None,
134
135
  }
135
136
  }
@@ -181,6 +181,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
181
181
  CommandSpec { name: "copilot", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "copilot" }, summary: "launch a Copilot leader", usage: "usage: team-agent copilot [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
182
182
  CommandSpec { name: "grok", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "grok" }, summary: "launch a Grok leader", usage: "usage: team-agent grok [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
183
183
  CommandSpec { name: "cursor", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "cursor" }, summary: "launch a Cursor leader", usage: "usage: team-agent cursor [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
184
+ CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "launch a Pi leader", usage: "usage: team-agent pi -- --model <exact> --thinking <effort>", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
184
185
 
185
186
  CommandSpec { name: "init", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::Init), summary: "legacy bootstrap command", usage: "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: Some("quick-start"), sunset: Some("C2"), action: Some("use `team-agent quick-start`"), governance: None },
186
187
  CommandSpec { name: "start", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::SpecOnlyAlias { alias_of: "quick-start" }, summary: "legacy start alias", usage: "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: Some("quick-start"), sunset: Some("C2"), action: Some("use `team-agent quick-start` or `team-agent restart`"), governance: None },
@@ -392,10 +392,17 @@ fn compile_role_agent_with_mode(
392
392
  let provider = required_string(&meta, role_path, "provider")?;
393
393
  require_explicit_grok_role_model(&meta, role_path, &provider)?;
394
394
  require_explicit_cursor_role_model(&meta, role_path, &provider)?;
395
+ require_explicit_pi_role_fields(&meta, role_path, &provider)?;
395
396
  let model = resolve_model(&meta, team_meta, &provider);
396
397
  let auth_mode = string_field(&meta, "auth_mode")
397
398
  .or_else(|| string_field(team_meta, "default_auth_mode"))
398
399
  .unwrap_or_else(|| "subscription".to_string());
400
+ if parse_canonical_provider(&provider) == Some(Provider::Pi) && auth_mode != "subscription" {
401
+ return Err(ModelError::Validation(format!(
402
+ "{}: Pi roles support subscription auth_mode only",
403
+ role_path.display()
404
+ )));
405
+ }
399
406
  if auth_mode != "subscription" && meta.get("profile").is_none() {
400
407
  return Err(ModelError::Validation(format!(
401
408
  "{}: profile is required when auth_mode is '{auth_mode}'",
@@ -478,7 +485,10 @@ fn compile_role_agent_with_mode(
478
485
  })
479
486
  .unwrap_or("");
480
487
  let provider_enum = parse_canonical_provider(provider_str).unwrap_or(Provider::Codex);
481
- if effort.is_claude_only() && !is_claude_family(provider_enum) {
488
+ if effort.is_claude_only()
489
+ && !is_claude_family(provider_enum)
490
+ && provider_enum != Provider::Pi
491
+ {
482
492
  return Err(ModelError::Validation(format!(
483
493
  "{}: effort '{}' is only supported by claude/claude_code (provider: {provider_str})",
484
494
  role_path.display(),
@@ -609,12 +619,59 @@ model: sonnet-4-thinking",
609
619
  )))
610
620
  }
611
621
 
612
- /// 0.5.66 bypass 单源:角色 md 的 `dangerously_skip_permissions` 必填 bool。
613
- /// 缺 = 编译失败(消息含硬串);非 bool = 同 fail-loud。
614
- fn required_dangerously_skip_permissions(
622
+ fn require_explicit_pi_role_fields(
615
623
  meta: &Value,
616
624
  path: &Path,
617
- ) -> Result<bool, ModelError> {
625
+ provider: &str,
626
+ ) -> Result<(), ModelError> {
627
+ if parse_canonical_provider(provider) != Some(Provider::Pi) {
628
+ return Ok(());
629
+ }
630
+ let model = string_field(meta, "model")
631
+ .filter(|value| !value.trim().is_empty())
632
+ .ok_or_else(|| {
633
+ ModelError::Validation(format!(
634
+ "{}: Pi roles require an explicit qualified exact model",
635
+ path.display()
636
+ ))
637
+ })?;
638
+ let (catalog, name) = model.trim().split_once('/').ok_or_else(|| {
639
+ ModelError::Validation(format!(
640
+ "{}: Pi roles require a qualified exact model such as provider/model",
641
+ path.display()
642
+ ))
643
+ })?;
644
+ if catalog.is_empty() || name.is_empty() || model.contains('*') {
645
+ return Err(ModelError::Validation(format!(
646
+ "{}: Pi roles require a qualified exact model",
647
+ path.display()
648
+ )));
649
+ }
650
+ if !string_field(meta, "effort").is_some_and(|value| !value.trim().is_empty()) {
651
+ return Err(ModelError::Validation(format!(
652
+ "{}: Pi roles require explicit effort",
653
+ path.display()
654
+ )));
655
+ }
656
+ let tools = required_tools(meta, path)?;
657
+ if !tools.iter().any(|tool| tool == "mcp_team") {
658
+ return Err(ModelError::Validation(format!(
659
+ "{}: Pi roles require mcp_team",
660
+ path.display()
661
+ )));
662
+ }
663
+ match meta.get("dangerously_skip_permissions") {
664
+ Some(Value::Bool(true)) => Ok(()),
665
+ _ => Err(ModelError::Validation(format!(
666
+ "{}: Pi roles require dangerously_skip_permissions: true",
667
+ path.display()
668
+ ))),
669
+ }
670
+ }
671
+
672
+ /// 0.5.66 bypass 单源:角色 md 的 `dangerously_skip_permissions` 必填 bool。
673
+ /// 缺 = 编译失败(消息含硬串);非 bool = 同 fail-loud。
674
+ fn required_dangerously_skip_permissions(meta: &Value, path: &Path) -> Result<bool, ModelError> {
618
675
  match meta.get("dangerously_skip_permissions") {
619
676
  Some(Value::Bool(value)) => Ok(*value),
620
677
  Some(_) => Err(ModelError::Validation(format!(
@@ -584,6 +584,7 @@ fn runtime_event_provider_name(provider: Provider) -> &'static str {
584
584
  Provider::GeminiCli => "gemini_cli",
585
585
  Provider::Grok => "grok",
586
586
  Provider::CursorAgent => "cursor_agent",
587
+ Provider::Pi => "pi",
587
588
  Provider::Fake => "fake",
588
589
  }
589
590
  }
@@ -19,6 +19,7 @@ pub fn provider_window_name(provider: Provider) -> &'static str {
19
19
  Provider::GeminiCli => "gemini",
20
20
  Provider::Grok => "grok",
21
21
  Provider::CursorAgent => "agent",
22
+ Provider::Pi => "pi",
22
23
  Provider::Fake => "fake",
23
24
  }
24
25
  }
@@ -285,10 +285,58 @@ fn leader_start_plan_with_ambient_authority(
285
285
  ),
286
286
  _ => None,
287
287
  };
288
+ let provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
289
+ let parsed = crate::lifecycle::launch::pi_mcp::parse_pi_leader_args(provider_args)
290
+ .map_err(|error| LeaderError::Start(error.to_string()))?;
291
+ let mcp_config = adapter
292
+ .mcp_config(crate::model::enums::AuthMode::Subscription)
293
+ .map_err(|error| LeaderError::Start(error.to_string()))?;
294
+ let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
295
+ mcp_config,
296
+ workspace,
297
+ "leader",
298
+ identity.team_id.as_str(),
299
+ );
300
+ let prompt = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
301
+ let tools = ["mcp_team", "fs_read", "fs_list", "fs_write", "execute_bash"];
302
+ let team_mcp_tools = [
303
+ "assign_task",
304
+ "send_message",
305
+ "update_state",
306
+ "get_team_status",
307
+ "stop_agent",
308
+ "reset_agent",
309
+ "add_agent",
310
+ "clone_agent",
311
+ "fork_agent",
312
+ "request_human",
313
+ "stuck_list",
314
+ "stuck_cancel",
315
+ ];
316
+ crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
317
+ crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
318
+ workspace,
319
+ team_id: identity.team_id.as_str(),
320
+ agent_id: "leader",
321
+ model: &parsed.model,
322
+ effort: parsed.effort,
323
+ system_prompt: &prompt,
324
+ tool_categories: &tools,
325
+ team_mcp_tools: &team_mcp_tools,
326
+ mcp_config: &mcp_config,
327
+ },
328
+ )
329
+ .map_err(|error| LeaderError::Start(error.to_string()))?
330
+ .argv
331
+ } else if provider == Provider::Pi {
332
+ Vec::new()
333
+ } else {
334
+ provider_command_argv(provider, provider_args)
335
+ };
288
336
  let argv = start_argv(
289
337
  mode,
290
338
  provider,
291
- provider_args,
339
+ &provider_argv,
292
340
  workspace,
293
341
  session_name.as_ref(),
294
342
  managed_window.as_ref(),
@@ -305,7 +353,6 @@ fn leader_start_plan_with_ambient_authority(
305
353
  } else {
306
354
  leader_env.clone()
307
355
  };
308
- let provider_argv = provider_command_argv(provider, provider_args);
309
356
  Ok(LeaderStartPlan {
310
357
  mode,
311
358
  provider,
@@ -970,20 +1017,15 @@ fn managed_leader_window_for_launch(
970
1017
  fn start_argv(
971
1018
  mode: LeaderStartMode,
972
1019
  provider: Provider,
973
- provider_args: &[String],
1020
+ provider_argv: &[String],
974
1021
  workspace: &Path,
975
1022
  session_name: Option<&SessionName>,
976
1023
  leader_window: Option<&WindowName>,
977
1024
  leader_env: &BTreeMap<String, String>,
978
1025
  managed_client_attach_mode: Option<ManagedClientAttachMode>,
979
1026
  ) -> Result<Vec<String>, LeaderError> {
980
- let provider_cmd = provider_command_name(provider).to_string();
981
1027
  match mode {
982
- LeaderStartMode::ExecProvider => {
983
- let mut argv = vec![provider_cmd];
984
- argv.extend(normalized_provider_args(provider_args));
985
- Ok(argv)
986
- }
1028
+ LeaderStartMode::ExecProvider => Ok(provider_argv.to_vec()),
987
1029
  LeaderStartMode::ManagedTmuxClient => {
988
1030
  let Some(session) = session_name else {
989
1031
  return Err(LeaderError::Start(
@@ -1021,13 +1063,11 @@ fn start_argv(
1021
1063
  if let Some(path) = std::env::var_os("PATH").and_then(|p| p.into_string().ok()) {
1022
1064
  exports.push(shlex_quote(&format!("PATH={path}")));
1023
1065
  }
1024
- let mut provider_argv = vec![provider_cmd];
1025
- provider_argv.extend(normalized_provider_args(provider_args));
1026
1066
  let shell = format!(
1027
1067
  "cd {} && export {} && exec {}",
1028
1068
  shlex_quote(&resolved_workspace.to_string_lossy()),
1029
1069
  exports.join(" "),
1030
- shell_join(&provider_argv)
1070
+ shell_join(provider_argv)
1031
1071
  );
1032
1072
  let argv = vec![
1033
1073
  "tmux".to_string(),
@@ -2777,6 +2817,7 @@ fn provider_command_name(provider: Provider) -> &'static str {
2777
2817
  Provider::GeminiCli => "gemini",
2778
2818
  Provider::Grok => "grok",
2779
2819
  Provider::CursorAgent => "agent",
2820
+ Provider::Pi => "pi",
2780
2821
  Provider::Fake => "fake",
2781
2822
  }
2782
2823
  }
@@ -20,7 +20,7 @@
20
20
  //! boundary:
21
21
  //! - 不拷贝外部角色文件进 team 目录,就地读取编译
22
22
  //! - 起席一律走 restart 的 start_agent_at_paths,本文件不直接 spawn
23
- //! - 回滚只恢复 spec 字节与 runtime state,不回收已 spawn 的 pane
23
+ //! - start path 按 exact pane receipt 回收已 spawn pane;本文件恢复 spec/state,并删除新 Pi 席位的 exact wrapper
24
24
  //! maturity: wired
25
25
  //! ---
26
26
  use std::collections::{BTreeMap, BTreeSet};
@@ -217,18 +217,14 @@ pub fn add_agent(
217
217
  ) {
218
218
  Ok(selected) => selected,
219
219
  Err(_) if workspace.join("TEAM.md").exists() => {
220
- // **0.3.24 add-agent socket drift fix**: even on the TEAM.md fallback
221
- // path (no spec yet), prefer the state-aware resolver. It reads the
222
- // team's persisted `tmux_endpoint` (set at `team-agent launch` time)
223
- // and routes the new agent's spawn to the SAME tmux socket the live
224
- // team uses. Cold workspaces / first-agent paths safely fall back to
225
- // `TmuxBackend::for_workspace(team_workspace)` inside the resolver.
226
- let team_ws = team_workspace(workspace);
220
+ // The MCP server already passes the canonical run workspace here.
221
+ // Reapplying team_workspace() would strip the scratch root and bind
222
+ // the first dynamic worker to the parent workspace's tmux server.
227
223
  let transport =
228
224
  crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
229
- &team_ws, team,
225
+ workspace, team,
230
226
  )
231
- .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
227
+ .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(workspace));
232
228
  return add_agent_with_transport(
233
229
  workspace,
234
230
  agent_id,
@@ -516,6 +512,28 @@ pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleErr
516
512
  )))
517
513
  }
518
514
 
515
+ fn rollback_added_pi_wrapper(
516
+ run_workspace: &Path,
517
+ team_key: &str,
518
+ agent_id: &AgentId,
519
+ role_meta: &Value,
520
+ ) -> Result<(), LifecycleError> {
521
+ if role_meta.get("provider").and_then(Value::as_str) != Some("pi") {
522
+ return Ok(());
523
+ }
524
+ let wrapper =
525
+ crate::lifecycle::launch::pi_mcp::pi_seat_paths(run_workspace, team_key, agent_id.as_str())
526
+ .wrapper;
527
+ match std::fs::remove_file(&wrapper) {
528
+ Ok(()) => Ok(()),
529
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
530
+ Err(error) => Err(LifecycleError::StatePersist(format!(
531
+ "failed to roll back added Pi wrapper {}: {error}",
532
+ wrapper.display()
533
+ ))),
534
+ }
535
+ }
536
+
519
537
  /// ---
520
538
  /// purpose: 加席的实体实现,含 owner 门、重名拒绝、重编译 spec 原子写、席位 state upsert 与起席
521
539
  /// params:
@@ -701,6 +719,8 @@ fn add_agent_with_transport_at_paths_reserved(
701
719
  ) {
702
720
  Ok(started) => started,
703
721
  Err(error) => {
722
+ let wrapper_rollback =
723
+ rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
704
724
  rollback_add_agent_atomic(
705
725
  run_workspace,
706
726
  &spec_path,
@@ -709,15 +729,42 @@ fn add_agent_with_transport_at_paths_reserved(
709
729
  agent_id,
710
730
  "start_agent_failed",
711
731
  );
712
- return Err(error);
732
+ return match wrapper_rollback {
733
+ Ok(()) => Err(error),
734
+ Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
735
+ "{error}; {rollback_error}"
736
+ ))),
737
+ };
713
738
  }
714
739
  };
715
740
  let (env, start_mode) = match started {
716
741
  StartAgentOutcome::Running {
717
742
  env, start_mode, ..
718
743
  } => (env, start_mode),
719
- StartAgentOutcome::Noop { env, .. } => (env, StartMode::Noop),
744
+ StartAgentOutcome::Noop { .. } => {
745
+ let wrapper_rollback =
746
+ rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
747
+ rollback_add_agent_atomic(
748
+ run_workspace,
749
+ &spec_path,
750
+ pre_spec_text.as_deref(),
751
+ None,
752
+ agent_id,
753
+ "added_agent_noop",
754
+ );
755
+ let error = LifecycleError::RequirementUnmet(format!(
756
+ "newly added agent {agent_id} returned start_agent.noop"
757
+ ));
758
+ return match wrapper_rollback {
759
+ Ok(()) => Err(error),
760
+ Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
761
+ "{error}; {rollback_error}"
762
+ ))),
763
+ };
764
+ }
720
765
  StartAgentOutcome::Paused { .. } => {
766
+ let wrapper_rollback =
767
+ rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
721
768
  rollback_add_agent_atomic(
722
769
  run_workspace,
723
770
  &spec_path,
@@ -726,9 +773,14 @@ fn add_agent_with_transport_at_paths_reserved(
726
773
  agent_id,
727
774
  "added_agent_paused",
728
775
  );
729
- return Err(LifecycleError::RequirementUnmet(format!(
730
- "added agent {agent_id} is paused"
731
- )));
776
+ let error =
777
+ LifecycleError::RequirementUnmet(format!("added agent {agent_id} is paused"));
778
+ return match wrapper_rollback {
779
+ Ok(()) => Err(error),
780
+ Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
781
+ "{error}; {rollback_error}"
782
+ ))),
783
+ };
732
784
  }
733
785
  };
734
786
  if let Some(reservation) = reservation {
@@ -167,6 +167,24 @@ pub(super) fn upsert_agent_state_from_role(
167
167
  "runtime state root is not an object".to_string(),
168
168
  ));
169
169
  };
170
+ if !root
171
+ .get("session_name")
172
+ .and_then(serde_json::Value::as_str)
173
+ .is_some_and(|session| !session.is_empty())
174
+ {
175
+ if let Some(owner_session) = root
176
+ .get("leader_receiver")
177
+ .and_then(|receiver| receiver.get("session_name"))
178
+ .and_then(serde_json::Value::as_str)
179
+ .filter(|session| !session.is_empty())
180
+ .map(str::to_string)
181
+ {
182
+ root.insert(
183
+ "session_name".to_string(),
184
+ serde_json::Value::String(owner_session),
185
+ );
186
+ }
187
+ }
170
188
  let agents = root
171
189
  .entry("agents".to_string())
172
190
  .or_insert_with(|| serde_json::json!({}));
@@ -259,6 +259,7 @@ fn provider_wire_str(provider: Provider) -> &'static str {
259
259
  Provider::GeminiCli => "gemini_cli",
260
260
  Provider::Grok => "grok",
261
261
  Provider::CursorAgent => "cursor_agent",
262
+ Provider::Pi => "pi",
262
263
  Provider::Fake => "fake",
263
264
  }
264
265
  }