@team-agent/installer 0.5.112 → 0.5.114

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 (77) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +119 -50
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +173 -109
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/leaders.rs +32 -32
  10. package/crates/team-agent/src/cli/mod.rs +158 -14
  11. package/crates/team-agent/src/cli/models.rs +68 -18
  12. package/crates/team-agent/src/cli/named_address.rs +17 -16
  13. package/crates/team-agent/src/cli/route.rs +4 -4
  14. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  15. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  16. package/crates/team-agent/src/cli/send.rs +4 -4
  17. package/crates/team-agent/src/cli/spec.rs +35 -29
  18. package/crates/team-agent/src/cli/status.rs +7 -7
  19. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  20. package/crates/team-agent/src/cli/tests/base.rs +94 -6
  21. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  22. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  23. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  24. package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
  25. package/crates/team-agent/src/cli/triage.rs +53 -2
  26. package/crates/team-agent/src/cli/types.rs +8 -8
  27. package/crates/team-agent/src/compiler/tests.rs +12 -0
  28. package/crates/team-agent/src/compiler.rs +65 -26
  29. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  30. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  31. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  32. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  33. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  34. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  35. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  36. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  37. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  38. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  39. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  40. package/crates/team-agent/src/leader/mod.rs +1 -0
  41. package/crates/team-agent/src/leader/prompt.rs +906 -0
  42. package/crates/team-agent/src/leader/start.rs +265 -16
  43. package/crates/team-agent/src/leader/tests/identity.rs +21 -0
  44. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  45. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  46. package/crates/team-agent/src/leader/types.rs +6 -0
  47. package/crates/team-agent/src/lib.rs +4 -0
  48. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  49. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  52. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  53. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  55. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  58. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  59. package/crates/team-agent/src/main.rs +6 -0
  60. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  61. package/crates/team-agent/src/messaging/send.rs +10 -0
  62. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  63. package/crates/team-agent/src/model/spec.rs +24 -12
  64. package/crates/team-agent/src/provider/adapter.rs +8 -59
  65. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  66. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  67. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  68. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  69. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  70. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  71. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  72. package/crates/team-agent/src/provider/mod.rs +1 -0
  73. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  74. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  75. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  76. package/crates/team-agent/src/state/persist.rs +73 -0
  77. package/package.json +4 -4
@@ -23,7 +23,7 @@ use crate::provider::{get_adapter, Provider};
23
23
  use crate::tmux_backend::TmuxBackend;
24
24
  use crate::transport::{
25
25
  CaptureRange, PaneId, PaneInfo, PaneLiveness, SessionName, SpawnResult, Target, Transport,
26
- WindowName,
26
+ TransportError, WindowName,
27
27
  };
28
28
 
29
29
  use super::helpers::{
@@ -291,6 +291,13 @@ fn leader_start_plan_with_ambient_authority(
291
291
  ),
292
292
  _ => None,
293
293
  };
294
+ let configured_prompt = if mode == LeaderStartMode::AttachExisting {
295
+ None
296
+ } else {
297
+ super::prompt::load().map_err(|error| error.for_provider(provider))?
298
+ };
299
+ let mut pi_routed_argv = None;
300
+ let mut session_capture = None;
294
301
  let mut provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
295
302
  let parsed = crate::lifecycle::launch::pi_mcp::parse_pi_leader_args(provider_args)
296
303
  .map_err(|error| LeaderError::Start(error.to_string()))?;
@@ -303,9 +310,23 @@ fn leader_start_plan_with_ambient_authority(
303
310
  "leader",
304
311
  identity.team_id.as_str(),
305
312
  );
306
- let prompt = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
313
+ let contract = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
314
+ let prompt = if let Some(stored) = configured_prompt.as_ref() {
315
+ // Read route once and validate conflicts before creating any Pi MCP artifacts.
316
+ let mut routed = vec!["pi".to_string()];
317
+ crate::provider::argv_route::apply(provider, &mut routed)
318
+ .map_err(|error| LeaderError::Start(error.to_string()))?;
319
+ let combined = super::prompt::compose_pi_contract(
320
+ &contract, &routed, &stored.text, &stored.path,
321
+ )?;
322
+ pi_routed_argv = Some(routed);
323
+ combined
324
+ } else {
325
+ contract
326
+ };
307
327
  let team_mcp_tools = ["send_message", "report_result", "get_team_status"];
308
- crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
328
+ let spawned_at = chrono::Utc::now().to_rfc3339();
329
+ let command = crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
309
330
  crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
310
331
  workspace,
311
332
  team_id: identity.team_id.as_str(),
@@ -322,16 +343,32 @@ fn leader_start_plan_with_ambient_authority(
322
343
  ),
323
344
  },
324
345
  )
325
- .map_err(|error| LeaderError::Start(error.to_string()))?
326
- .argv
346
+ .map_err(|error| LeaderError::Start(error.to_string()))?;
347
+ session_capture = Some(crate::provider::CaptureSessionContext {
348
+ agent_id: "leader".into(),
349
+ spawn_cwd: resolve_workspace_for_hash(workspace),
350
+ pane_id: None,
351
+ pane_pid: None,
352
+ spawned_at: Some(spawned_at),
353
+ expected_session_id: command.expected_session_id,
354
+ provider_projects_root: command.provider_projects_root,
355
+ });
356
+ command.argv
327
357
  } else if provider == Provider::Pi {
328
358
  Vec::new()
329
359
  } else {
330
360
  provider_command_argv(provider, provider_args)
331
361
  };
332
362
  if mode != LeaderStartMode::AttachExisting {
333
- crate::provider::argv_route::apply(provider, &mut provider_argv)
334
- .map_err(|error| LeaderError::Start(error.to_string()))?;
363
+ if let Some(routed) = pi_routed_argv {
364
+ provider_argv.splice(1..1, routed.into_iter().skip(1));
365
+ } else {
366
+ crate::provider::argv_route::apply(provider, &mut provider_argv)
367
+ .map_err(|error| LeaderError::Start(error.to_string()))?;
368
+ if let Some(stored) = configured_prompt.as_ref() {
369
+ super::prompt::inject(provider, &mut provider_argv, &stored.text, &stored.path)?;
370
+ }
371
+ }
335
372
  }
336
373
  let argv = start_argv(
337
374
  mode,
@@ -361,6 +398,10 @@ fn leader_start_plan_with_ambient_authority(
361
398
  session_name: plan_session_name,
362
399
  argv,
363
400
  provider_argv,
401
+ leader_prompt: configured_prompt
402
+ .as_ref()
403
+ .map(|stored| stored.metadata(provider)),
404
+ session_capture,
364
405
  // 0.3.28 Step 2: leader window inside the dedicated leader session is
365
406
  // named after the provider wire (e.g. `claude_code`), never the
366
407
  // literal string `leader`. Occupied names get a per-launch suffix.
@@ -403,6 +444,11 @@ pub(crate) fn leader_env_for_identity(
403
444
  "1".to_string(),
404
445
  );
405
446
  }
447
+ // Managed providers run after `sh -lc`; restore the launcher's native
448
+ // executable lookup path at invocation, not just in the tmux client env.
449
+ if let Some(path) = std::env::var_os("PATH").and_then(|path| path.into_string().ok()) {
450
+ leader_env.insert("PATH".to_string(), path);
451
+ }
406
452
  leader_env
407
453
  }
408
454
 
@@ -1340,7 +1386,7 @@ fn ensure_managed_leader_pane(
1340
1386
  &env_unset,
1341
1387
  provider_label,
1342
1388
  )
1343
- .map_err(|error| LeaderError::Start(error.to_string()))
1389
+ .map_err(|error| prompt_aware_spawn_error(plan, error))
1344
1390
  } else {
1345
1391
  let env_unset = leader_env_unset_for_provider(plan.provider);
1346
1392
  let provider_label = provider_command_name(plan.provider);
@@ -1354,8 +1400,35 @@ fn ensure_managed_leader_pane(
1354
1400
  &env_unset,
1355
1401
  provider_label,
1356
1402
  )
1357
- .map_err(|error| LeaderError::Start(error.to_string()))
1403
+ .map_err(|error| prompt_aware_spawn_error(plan, error))
1404
+ }
1405
+ }
1406
+
1407
+ fn prompt_aware_spawn_error(plan: &LeaderStartPlan, error: TransportError) -> LeaderError {
1408
+ let Some(metadata) = plan.leader_prompt.as_ref() else {
1409
+ return LeaderError::Start(error.to_string());
1410
+ };
1411
+ // Transport subprocess errors contain the full tmux shell argv. Preserve
1412
+ // real exit/I/O evidence without exposing the process-level prompt payload.
1413
+ let mut safe = super::prompt::PromptError::new(
1414
+ "leader_prompt_carrier_failed",
1415
+ "Leader prompt-bearing transport spawn failed; argv and stderr withheld",
1416
+ Some(&metadata.config_path),
1417
+ )
1418
+ .for_provider(plan.provider);
1419
+ match error {
1420
+ TransportError::Subprocess { code, .. } => safe.exit_code = code,
1421
+ TransportError::Spawn { source, .. } | TransportError::Inject { source, .. }
1422
+ | TransportError::Capture { source } | TransportError::Io(source) => {
1423
+ safe.io_kind = Some(format!("{:?}", source.kind()));
1424
+ }
1425
+ oversized @ TransportError::CommandTooLong { .. } => {
1426
+ safe.error = oversized.to_string();
1427
+ }
1428
+ _ => {}
1358
1429
  }
1430
+ safe.action = "Inspect the owned launcher and transport metadata; do not repeat or change carriers to bypass the failure";
1431
+ LeaderError::Prompt(safe)
1359
1432
  }
1360
1433
 
1361
1434
  struct ManagedAttachWatch<'a> {
@@ -1735,6 +1808,25 @@ fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnRes
1735
1808
  })
1736
1809
  }
1737
1810
 
1811
+ /// Keep the launcher's native identity with the canonical pane. The backing
1812
+ /// JSONL may not exist until Pi's first turn; receipt observation resolves it by
1813
+ /// exact header, never by filename/mtime or the Team Agent owner UUID.
1814
+ fn bind_plan_session(receiver: &mut serde_json::Value, plan: &LeaderStartPlan) {
1815
+ if let Some(capture) = &plan.session_capture {
1816
+ let native = serde_json::json!({
1817
+ "expected_session_id": capture.expected_session_id,
1818
+ "provider_projects_root": capture.provider_projects_root,
1819
+ "spawn_cwd": capture.spawn_cwd,
1820
+ "spawned_at": capture.spawned_at,
1821
+ "pane_id": receiver.get("pane_id"),
1822
+ "tmux_socket": receiver.get("tmux_socket"),
1823
+ "leader_session_uuid": receiver.get("leader_session_uuid"),
1824
+ "owner_epoch": receiver.get("owner_epoch"),
1825
+ });
1826
+ receiver["native_session"] = native;
1827
+ }
1828
+ }
1829
+
1738
1830
  fn persist_managed_leader_binding(
1739
1831
  plan: &LeaderStartPlan,
1740
1832
  workspace: &Path,
@@ -1850,7 +1942,7 @@ fn persist_managed_leader_binding(
1850
1942
  } else {
1851
1943
  serde_json::json!("managed_launcher")
1852
1944
  };
1853
- let receiver = serde_json::json!({
1945
+ let mut receiver = serde_json::json!({
1854
1946
  "mode": "direct_tmux",
1855
1947
  "status": "attached",
1856
1948
  "provider": provider.clone(),
@@ -1865,6 +1957,15 @@ fn persist_managed_leader_binding(
1865
1957
  "discovery": discovery,
1866
1958
  "bound_panes": bound_panes,
1867
1959
  });
1960
+ if keep_existing_scalar {
1961
+ for key in ["native_session", "rollout_path"] {
1962
+ if let Some(value) = existing_recv.as_ref().and_then(|old| old.get(key)) {
1963
+ receiver[key] = value.clone();
1964
+ }
1965
+ }
1966
+ } else {
1967
+ bind_plan_session(&mut receiver, plan);
1968
+ }
1868
1969
  let owner_uuid = if keep_existing_scalar {
1869
1970
  keep_field(
1870
1971
  existing_owner.as_ref(),
@@ -2023,6 +2124,7 @@ fn persist_exec_provider_leader_binding(
2023
2124
  }
2024
2125
  obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
2025
2126
  }
2127
+ bind_plan_session(&mut receiver, plan);
2026
2128
  let owner = serde_json::json!({
2027
2129
  "pane_id": pane,
2028
2130
  "provider": provider.clone(),
@@ -2416,6 +2518,11 @@ fn run_leader_argv(
2416
2518
  diagnostics_path.display()
2417
2519
  ))
2418
2520
  })?;
2521
+ // Captured transport diagnostics can echo the shell argv carrying the
2522
+ // prompt. Do not tee those bytes before redaction. Native ExecProvider
2523
+ // business streams and inherited TTY stdout keep their existing behavior.
2524
+ let withhold_transport_output = plan.leader_prompt.is_some()
2525
+ && plan.mode != LeaderStartMode::ExecProvider;
2419
2526
  let stdout_reader = child.stdout.take().map(|mut child_stdout| {
2420
2527
  std::thread::spawn(move || {
2421
2528
  let mut captured = Vec::new();
@@ -2427,7 +2534,9 @@ fn run_leader_argv(
2427
2534
  if read == 0 {
2428
2535
  break;
2429
2536
  }
2430
- let _ = std::io::stdout().write_all(&buf[..read]);
2537
+ if !withhold_transport_output {
2538
+ let _ = std::io::stdout().write_all(&buf[..read]);
2539
+ }
2431
2540
  captured.extend_from_slice(&buf[..read]);
2432
2541
  }
2433
2542
  captured
@@ -2444,7 +2553,9 @@ fn run_leader_argv(
2444
2553
  if read == 0 {
2445
2554
  break;
2446
2555
  }
2447
- let _ = std::io::stderr().write_all(&buf[..read]);
2556
+ if !withhold_transport_output {
2557
+ let _ = std::io::stderr().write_all(&buf[..read]);
2558
+ }
2448
2559
  captured.extend_from_slice(&buf[..read]);
2449
2560
  }
2450
2561
  captured
@@ -2482,8 +2593,8 @@ fn run_leader_argv(
2482
2593
  .code()
2483
2594
  .map(|code| code.to_string())
2484
2595
  .unwrap_or_else(|| "signal".to_string()),
2485
- crate::redaction::redact_external_text(&stdout),
2486
- crate::redaction::redact_external_text(&stderr),
2596
+ prompt_aware_diagnostic_text(plan, &stdout),
2597
+ prompt_aware_diagnostic_text(plan, &stderr),
2487
2598
  ),
2488
2599
  )
2489
2600
  .map_err(|error| {
@@ -2492,6 +2603,11 @@ fn run_leader_argv(
2492
2603
  diagnostics_path.display()
2493
2604
  ))
2494
2605
  })?;
2606
+ let stderr = if plan.leader_prompt.is_some() {
2607
+ prompt_aware_diagnostic_text(plan, &stderr)
2608
+ } else {
2609
+ stderr
2610
+ };
2495
2611
  Ok(LeaderProcessExit {
2496
2612
  status,
2497
2613
  stderr,
@@ -2586,6 +2702,14 @@ fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
2586
2702
  ))
2587
2703
  }
2588
2704
 
2705
+ fn prompt_aware_diagnostic_text(plan: &LeaderStartPlan, text: &str) -> String {
2706
+ if plan.leader_prompt.is_some() {
2707
+ "[withheld: Leader prompt-bearing launch]".to_string()
2708
+ } else {
2709
+ crate::redaction::redact_external_text(text)
2710
+ }
2711
+ }
2712
+
2589
2713
  fn write_launcher_diagnostics_header(
2590
2714
  path: &Path,
2591
2715
  plan: &LeaderStartPlan,
@@ -2594,13 +2718,22 @@ fn write_launcher_diagnostics_header(
2594
2718
  if let Some(parent) = path.parent() {
2595
2719
  std::fs::create_dir_all(parent)?;
2596
2720
  }
2721
+ let metadata = plan
2722
+ .leader_prompt
2723
+ .as_ref()
2724
+ .map(serde_json::to_string)
2725
+ .transpose()
2726
+ .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?
2727
+ .map(|metadata| format!("leader_prompt={metadata}\n"))
2728
+ .unwrap_or_default();
2597
2729
  std::fs::write(
2598
2730
  path,
2599
2731
  format!(
2600
- "startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
2732
+ "startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n{}",
2601
2733
  plan.mode,
2602
2734
  provider_wire(plan.provider),
2603
- crate::redaction::redact_external_text(&argv.join(" ")),
2735
+ prompt_aware_diagnostic_text(plan, &argv.join(" ")),
2736
+ metadata,
2604
2737
  ),
2605
2738
  )
2606
2739
  }
@@ -3340,6 +3473,8 @@ mod tests {
3340
3473
  session_name: None,
3341
3474
  argv: vec!["sh".to_string(), "-c".to_string(), command],
3342
3475
  provider_argv: vec!["codex".to_string()],
3476
+ leader_prompt: None,
3477
+ session_capture: None,
3343
3478
  leader_window: None,
3344
3479
  is_external_leader: true,
3345
3480
  leader_env: BTreeMap::new(),
@@ -3397,6 +3532,8 @@ mod tests {
3397
3532
  session_name: None,
3398
3533
  argv: vec!["sh".to_string(), "-c".to_string(), command],
3399
3534
  provider_argv: vec!["fake".to_string()],
3535
+ leader_prompt: None,
3536
+ session_capture: None,
3400
3537
  leader_window: None,
3401
3538
  is_external_leader: false,
3402
3539
  leader_env: BTreeMap::new(),
@@ -3926,6 +4063,8 @@ mod tests {
3926
4063
  session_name: Some(SessionName::new("team-agent-leader-claude_code-demo")),
3927
4064
  argv: vec!["tmux".to_string(), "attach-session".to_string()],
3928
4065
  provider_argv: vec!["claude".to_string()],
4066
+ leader_prompt: None,
4067
+ session_capture: None,
3929
4068
  leader_window: Some(WindowName::new("claude_code")),
3930
4069
  is_external_leader: false,
3931
4070
  leader_env: BTreeMap::new(),
@@ -3934,6 +4073,74 @@ mod tests {
3934
4073
  }
3935
4074
  }
3936
4075
 
4076
+ #[cfg(unix)]
4077
+ #[test]
4078
+ fn leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec() {
4079
+ const TEST: &str = "leader::start::tests::leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec";
4080
+ const KEY: &str = "TEAM_AGENT_TEST_PROMPT_CAPTURE_CASE";
4081
+ const BODY: &str = "SENSITIVE_PROMPT_309_CAPTURE";
4082
+ if let Ok(case) = std::env::var(KEY) {
4083
+ let root = std::env::temp_dir().join(format!("ta-prompt-capture-{}-{case}", std::process::id()));
4084
+ std::fs::create_dir_all(&root).unwrap();
4085
+ let mut plan = persist_test_plan(&root);
4086
+ plan.mode = if case == "exec_configured" { LeaderStartMode::ExecProvider } else { LeaderStartMode::NewTmuxSession };
4087
+ if case != "transport_absent" {
4088
+ plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
4089
+ config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
4090
+ });
4091
+ }
4092
+ // Controlled launcher echoes its received argv and fails; no native Agent/Team.
4093
+ let argv = vec!["/bin/sh".to_string(), "-c".to_string(),
4094
+ "printf '%s\\n' \"$1\"; printf '%s\\n' \"$1\" >&2; exit 23".to_string(), "launcher".to_string(), BODY.to_string()];
4095
+ let outcome = super::run_leader_argv(&argv, &BTreeMap::new(), &plan, &root, None).unwrap();
4096
+ assert_eq!(outcome.status.code(), Some(23));
4097
+ if plan.leader_prompt.is_some() { assert!(!outcome.stderr.contains(BODY)); }
4098
+ std::fs::remove_dir_all(root).unwrap();
4099
+ return;
4100
+ }
4101
+ for case in ["transport_configured", "transport_absent", "exec_configured"] {
4102
+ let output = std::process::Command::new(std::env::current_exe().unwrap())
4103
+ .args(["--exact", TEST, "--nocapture", "--test-threads=1"])
4104
+ .env(KEY, case).env_remove("TMUX").env_remove("TMUX_PANE")
4105
+ .output().unwrap();
4106
+ assert!(output.status.success(), "controlled child failed: {:?} {}", output.status, String::from_utf8_lossy(&output.stderr));
4107
+ for bytes in [&output.stdout, &output.stderr] {
4108
+ assert_eq!(String::from_utf8_lossy(bytes).contains(BODY), case != "transport_configured", "capture policy differs for {case}");
4109
+ }
4110
+ }
4111
+ }
4112
+
4113
+ #[test]
4114
+ fn leader_prompt_diagnostics_hide_payload_but_keep_unconfigured_baseline_and_native_cause() {
4115
+ let root = std::env::temp_dir().join(format!("ta-prompt-diagnostics-{}", std::process::id()));
4116
+ std::fs::create_dir_all(&root).unwrap();
4117
+ let path = root.join("launcher.log");
4118
+ let mut plan = persist_test_plan(&root);
4119
+ let plain = vec!["tmux".to_string(), "attach-session".to_string()];
4120
+ super::write_launcher_diagnostics_header(&path, &plan, &plain).unwrap();
4121
+ assert_eq!(std::fs::read_to_string(&path).unwrap(),
4122
+ "startup_stage=prepare\nmode=ManagedTmuxClient\nprovider=claude_code\nargv=tmux attach-session\n");
4123
+ const BODY: &str = "LEADER_ONLY_SENSITIVE_309";
4124
+ plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
4125
+ config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
4126
+ });
4127
+ let shell = vec!["tmux".to_string(), "new-session".to_string(),
4128
+ format!("claude --append-system-prompt '{BODY}'")];
4129
+ super::write_launcher_diagnostics_header(&path, &plan, &shell).unwrap();
4130
+ let header = std::fs::read_to_string(&path).unwrap();
4131
+ assert!(!header.contains(BODY));
4132
+ assert!(header.contains("leader_prompt={") && header.contains("--append-system-prompt"));
4133
+ assert!(!super::prompt_aware_diagnostic_text(&plan, BODY).contains(BODY));
4134
+ let error = super::prompt_aware_spawn_error(&plan, TransportError::Subprocess {
4135
+ argv: shell, code: Some(7), stderr: BODY.to_string(),
4136
+ });
4137
+ let crate::leader::LeaderError::Prompt(error) = error else { panic!("typed carrier failure required") };
4138
+ assert_eq!(error.reason, "leader_prompt_carrier_failed");
4139
+ assert_eq!(error.exit_code, Some(7));
4140
+ assert!(!error.report().to_string().contains(BODY));
4141
+ std::fs::remove_dir_all(root).unwrap();
4142
+ }
4143
+
3937
4144
  #[test]
3938
4145
  fn multi_leader_window_keeps_base_when_free() {
3939
4146
  let window =
@@ -4015,6 +4222,48 @@ mod tests {
4015
4222
  let _ = std::fs::remove_dir_all(&workspace);
4016
4223
  }
4017
4224
 
4225
+ #[test]
4226
+ fn pi_managed_leader_persists_native_identity_with_the_canonical_pane() {
4227
+ let workspace = std::env::temp_dir().join(format!(
4228
+ "ta-pi-leader-native-binding-{}", std::process::id()
4229
+ ));
4230
+ std::fs::create_dir_all(workspace.join(".team/runtime")).unwrap();
4231
+ let mut plan = persist_test_plan(&workspace);
4232
+ plan.provider = Provider::Pi;
4233
+ plan.session_capture = Some(crate::provider::CaptureSessionContext {
4234
+ agent_id: "leader".into(),
4235
+ spawn_cwd: workspace.clone(),
4236
+ pane_id: None,
4237
+ pane_pid: None,
4238
+ spawned_at: Some("2026-10-10T03:59:07Z".into()),
4239
+ expected_session_id: Some(crate::provider::SessionId::new("native-first")),
4240
+ provider_projects_root: Some(workspace.join("pi-sessions")),
4241
+ });
4242
+ let mut pane = SpawnResult {
4243
+ pane_id: PaneId::new("%0"),
4244
+ session: SessionName::new("team-agent-leader-pi-demo"),
4245
+ window: WindowName::new("pi"),
4246
+ child_pid: Some(1),
4247
+ };
4248
+ persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
4249
+ let first = crate::state::persist::load_runtime_state(&workspace).unwrap();
4250
+ let receiver = &first["teams"]["current"]["leader_receiver"];
4251
+ let native = &receiver["native_session"];
4252
+ assert_eq!(native["expected_session_id"], "native-first");
4253
+ assert_ne!(native["expected_session_id"], receiver["leader_session_uuid"]);
4254
+ assert_eq!(native["provider_projects_root"], serde_json::json!(workspace.join("pi-sessions")));
4255
+ for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
4256
+ assert_eq!(native[key], receiver[key]);
4257
+ }
4258
+ pane.pane_id = PaneId::new("%1");
4259
+ plan.session_capture.as_mut().unwrap().expected_session_id =
4260
+ Some(crate::provider::SessionId::new("native-second"));
4261
+ persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
4262
+ let second = crate::state::persist::load_runtime_state(&workspace).unwrap();
4263
+ assert_eq!(second["teams"]["current"]["leader_receiver"]["native_session"], *native);
4264
+ let _ = std::fs::remove_dir_all(workspace);
4265
+ }
4266
+
4018
4267
  #[test]
4019
4268
  fn multi_leader_persist_keeps_first_owner_uuid() {
4020
4269
  let workspace = std::env::temp_dir().join(format!(
@@ -291,6 +291,27 @@ fn copilot_leader_env_disables_terminal_title_only_for_copilot() {
291
291
  }
292
292
  }
293
293
 
294
+ #[test]
295
+ #[serial_test::serial(env)]
296
+ fn leader_env_preserves_native_path_for_managed_shell_wrapper() {
297
+ let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
298
+ let native_path = "/tmp/native leader/bin:/usr/bin:/bin";
299
+ let _env = EnvGuard::apply(&[("PATH", Some(native_path))]);
300
+ let ws = p2_temp_ws("native_path");
301
+ let identity = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
302
+ let env = leader_env_for_identity(Provider::Pi, &identity);
303
+ assert_eq!(env.get("PATH").map(String::as_str), Some(native_path));
304
+ let command = crate::tmux_backend::leader_shell_wrapper_command(
305
+ &["pi".to_string(), "--name".to_string(), "leader".to_string()],
306
+ &ws, &env, &[], "pi",
307
+ );
308
+ assert!(command.contains("PATH='/tmp/native leader/bin:/usr/bin:/bin'"));
309
+ assert!(command.find("PATH=").unwrap() < command.find(" pi --name leader").unwrap());
310
+ assert_eq!(env.get("TEAM_AGENT_TEAM_ID").map(String::as_str), Some(identity.team_id.as_str()));
311
+ let _unset = EnvGuard::apply(&[("PATH", None)]);
312
+ assert!(!leader_env_for_identity(Provider::Pi, &identity).contains_key("PATH"));
313
+ }
314
+
294
315
  // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
295
316
  // Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
296
317
  // Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
@@ -1,6 +1,8 @@
1
1
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
2
  use super::*;
3
3
 
4
+ mod prompt_contract;
5
+
4
6
  // ── helpers ────────────────────────────────────────────────────────────
5
7
  /// derive 一个真 LeaderSessionUuid(骨架无公开裸构造器,仅 `derive`)。
6
8
  fn uuid(fp: &str, ws: &str, user: &str, team: &str) -> LeaderSessionUuid {