@team-agent/installer 0.5.113 → 0.5.115

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 (52) 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 +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +57 -22
  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/mod.rs +157 -8
  10. package/crates/team-agent/src/cli/models.rs +119 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
  17. package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
  18. package/crates/team-agent/src/contract_runtime/config.rs +340 -0
  19. package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
  20. package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
  21. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  22. package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
  23. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  24. package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
  25. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  26. package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
  27. package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
  28. package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
  29. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  30. package/crates/team-agent/src/leader/mod.rs +1 -0
  31. package/crates/team-agent/src/leader/prompt.rs +906 -0
  32. package/crates/team-agent/src/leader/start.rs +353 -21
  33. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  34. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  35. package/crates/team-agent/src/leader/types.rs +6 -0
  36. package/crates/team-agent/src/lib.rs +4 -0
  37. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
  38. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  39. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
  40. package/crates/team-agent/src/lifecycle/launch.rs +45 -28
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
  42. package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
  43. package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
  44. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  45. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  46. package/crates/team-agent/src/main.rs +6 -0
  47. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  48. package/crates/team-agent/src/messaging/send.rs +10 -0
  49. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  50. package/crates/team-agent/src/model/spec.rs +24 -12
  51. package/crates/team-agent/src/state/persist.rs +73 -0
  52. 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.
@@ -1345,7 +1386,7 @@ fn ensure_managed_leader_pane(
1345
1386
  &env_unset,
1346
1387
  provider_label,
1347
1388
  )
1348
- .map_err(|error| LeaderError::Start(error.to_string()))
1389
+ .map_err(|error| prompt_aware_spawn_error(plan, error))
1349
1390
  } else {
1350
1391
  let env_unset = leader_env_unset_for_provider(plan.provider);
1351
1392
  let provider_label = provider_command_name(plan.provider);
@@ -1359,8 +1400,35 @@ fn ensure_managed_leader_pane(
1359
1400
  &env_unset,
1360
1401
  provider_label,
1361
1402
  )
1362
- .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
+ _ => {}
1363
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)
1364
1432
  }
1365
1433
 
1366
1434
  struct ManagedAttachWatch<'a> {
@@ -1740,6 +1808,25 @@ fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnRes
1740
1808
  })
1741
1809
  }
1742
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
+
1743
1830
  fn persist_managed_leader_binding(
1744
1831
  plan: &LeaderStartPlan,
1745
1832
  workspace: &Path,
@@ -1855,7 +1942,7 @@ fn persist_managed_leader_binding(
1855
1942
  } else {
1856
1943
  serde_json::json!("managed_launcher")
1857
1944
  };
1858
- let receiver = serde_json::json!({
1945
+ let mut receiver = serde_json::json!({
1859
1946
  "mode": "direct_tmux",
1860
1947
  "status": "attached",
1861
1948
  "provider": provider.clone(),
@@ -1870,6 +1957,15 @@ fn persist_managed_leader_binding(
1870
1957
  "discovery": discovery,
1871
1958
  "bound_panes": bound_panes,
1872
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
+ }
1873
1969
  let owner_uuid = if keep_existing_scalar {
1874
1970
  keep_field(
1875
1971
  existing_owner.as_ref(),
@@ -2028,6 +2124,7 @@ fn persist_exec_provider_leader_binding(
2028
2124
  }
2029
2125
  obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
2030
2126
  }
2127
+ bind_plan_session(&mut receiver, plan);
2031
2128
  let owner = serde_json::json!({
2032
2129
  "pane_id": pane,
2033
2130
  "provider": provider.clone(),
@@ -2043,10 +2140,8 @@ fn persist_exec_provider_leader_binding(
2043
2140
  "active_team_key".to_string(),
2044
2141
  serde_json::json!(identity.team_id.as_str()),
2045
2142
  );
2046
- obj.insert(
2047
- "session_name".to_string(),
2048
- serde_json::json!(target.session.as_str()),
2049
- );
2143
+ // The ambient session identifies the leader receiver, not workers.
2144
+ // Leave any existing worker session untouched, including unknown state.
2050
2145
  obj.insert("tmux_endpoint".to_string(), serde_json::json!(socket));
2051
2146
  obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
2052
2147
  obj.insert(
@@ -2421,6 +2516,11 @@ fn run_leader_argv(
2421
2516
  diagnostics_path.display()
2422
2517
  ))
2423
2518
  })?;
2519
+ // Captured transport diagnostics can echo the shell argv carrying the
2520
+ // prompt. Do not tee those bytes before redaction. Native ExecProvider
2521
+ // business streams and inherited TTY stdout keep their existing behavior.
2522
+ let withhold_transport_output = plan.leader_prompt.is_some()
2523
+ && plan.mode != LeaderStartMode::ExecProvider;
2424
2524
  let stdout_reader = child.stdout.take().map(|mut child_stdout| {
2425
2525
  std::thread::spawn(move || {
2426
2526
  let mut captured = Vec::new();
@@ -2432,7 +2532,9 @@ fn run_leader_argv(
2432
2532
  if read == 0 {
2433
2533
  break;
2434
2534
  }
2435
- let _ = std::io::stdout().write_all(&buf[..read]);
2535
+ if !withhold_transport_output {
2536
+ let _ = std::io::stdout().write_all(&buf[..read]);
2537
+ }
2436
2538
  captured.extend_from_slice(&buf[..read]);
2437
2539
  }
2438
2540
  captured
@@ -2449,7 +2551,9 @@ fn run_leader_argv(
2449
2551
  if read == 0 {
2450
2552
  break;
2451
2553
  }
2452
- let _ = std::io::stderr().write_all(&buf[..read]);
2554
+ if !withhold_transport_output {
2555
+ let _ = std::io::stderr().write_all(&buf[..read]);
2556
+ }
2453
2557
  captured.extend_from_slice(&buf[..read]);
2454
2558
  }
2455
2559
  captured
@@ -2487,8 +2591,8 @@ fn run_leader_argv(
2487
2591
  .code()
2488
2592
  .map(|code| code.to_string())
2489
2593
  .unwrap_or_else(|| "signal".to_string()),
2490
- crate::redaction::redact_external_text(&stdout),
2491
- crate::redaction::redact_external_text(&stderr),
2594
+ prompt_aware_diagnostic_text(plan, &stdout),
2595
+ prompt_aware_diagnostic_text(plan, &stderr),
2492
2596
  ),
2493
2597
  )
2494
2598
  .map_err(|error| {
@@ -2497,6 +2601,11 @@ fn run_leader_argv(
2497
2601
  diagnostics_path.display()
2498
2602
  ))
2499
2603
  })?;
2604
+ let stderr = if plan.leader_prompt.is_some() {
2605
+ prompt_aware_diagnostic_text(plan, &stderr)
2606
+ } else {
2607
+ stderr
2608
+ };
2500
2609
  Ok(LeaderProcessExit {
2501
2610
  status,
2502
2611
  stderr,
@@ -2591,6 +2700,14 @@ fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
2591
2700
  ))
2592
2701
  }
2593
2702
 
2703
+ fn prompt_aware_diagnostic_text(plan: &LeaderStartPlan, text: &str) -> String {
2704
+ if plan.leader_prompt.is_some() {
2705
+ "[withheld: Leader prompt-bearing launch]".to_string()
2706
+ } else {
2707
+ crate::redaction::redact_external_text(text)
2708
+ }
2709
+ }
2710
+
2594
2711
  fn write_launcher_diagnostics_header(
2595
2712
  path: &Path,
2596
2713
  plan: &LeaderStartPlan,
@@ -2599,13 +2716,22 @@ fn write_launcher_diagnostics_header(
2599
2716
  if let Some(parent) = path.parent() {
2600
2717
  std::fs::create_dir_all(parent)?;
2601
2718
  }
2719
+ let metadata = plan
2720
+ .leader_prompt
2721
+ .as_ref()
2722
+ .map(serde_json::to_string)
2723
+ .transpose()
2724
+ .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?
2725
+ .map(|metadata| format!("leader_prompt={metadata}\n"))
2726
+ .unwrap_or_default();
2602
2727
  std::fs::write(
2603
2728
  path,
2604
2729
  format!(
2605
- "startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
2730
+ "startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n{}",
2606
2731
  plan.mode,
2607
2732
  provider_wire(plan.provider),
2608
- crate::redaction::redact_external_text(&argv.join(" ")),
2733
+ prompt_aware_diagnostic_text(plan, &argv.join(" ")),
2734
+ metadata,
2609
2735
  ),
2610
2736
  )
2611
2737
  }
@@ -2934,7 +3060,8 @@ mod tests {
2934
3060
  execute_leader_plan_after_ambient_authority, format_launcher_failure,
2935
3061
  handle_exec_provider_startup_prompts, is_interactive_shell_basename,
2936
3062
  lock_down_window_rename, managed_client_argv, managed_leader_window_from_occupied,
2937
- persist_managed_leader_binding, push_bounded_stderr, shlex_quote,
3063
+ persist_exec_provider_leader_binding, persist_managed_leader_binding,
3064
+ push_bounded_stderr, shlex_quote,
2938
3065
  write_window_rename_lockdown_event, ManagedClientAttachMode, VerifiedAmbientPaneAuthority,
2939
3066
  LEADER_STDERR_LIMIT,
2940
3067
  };
@@ -3345,6 +3472,8 @@ mod tests {
3345
3472
  session_name: None,
3346
3473
  argv: vec!["sh".to_string(), "-c".to_string(), command],
3347
3474
  provider_argv: vec!["codex".to_string()],
3475
+ leader_prompt: None,
3476
+ session_capture: None,
3348
3477
  leader_window: None,
3349
3478
  is_external_leader: true,
3350
3479
  leader_env: BTreeMap::new(),
@@ -3365,6 +3494,95 @@ mod tests {
3365
3494
  let _ = std::fs::remove_dir_all(&workspace);
3366
3495
  }
3367
3496
 
3497
+ fn exec_binding_fixture(workspace: &Path) -> (LeaderStartPlan, VerifiedAmbientPaneAuthority) {
3498
+ let identity = LeaderIdentity {
3499
+ leader_session_uuid: LeaderSessionUuid::derive(
3500
+ "fp", &workspace.to_string_lossy(), "tester", "current",
3501
+ ).unwrap(),
3502
+ leader_session_uuid_source: LeaderSessionUuidSource::Derived,
3503
+ machine_fingerprint: "fp".into(), workspace_abspath: workspace.into(),
3504
+ os_user: "tester".into(), team_id: TeamKey::new("current"),
3505
+ };
3506
+ let plan = LeaderStartPlan {
3507
+ mode: LeaderStartMode::ExecProvider, provider: Provider::Pi,
3508
+ workspace: workspace.into(), socket: LeaderLaunchSocket::Workspace,
3509
+ session_name: None, argv: vec![], provider_argv: vec!["pi".into()],
3510
+ leader_prompt: None, session_capture: None, leader_window: None,
3511
+ is_external_leader: false, leader_env: BTreeMap::new(),
3512
+ identity: Some(identity), detached: false,
3513
+ };
3514
+ let authority = VerifiedAmbientPaneAuthority {
3515
+ pane_id: PaneId::new("%77"),
3516
+ observed: PaneInfo {
3517
+ pane_id: PaneId::new("%77"), session: SessionName::new("cursor-gate-leader"),
3518
+ window_index: Some(0), window_name: Some(WindowName::new("pi")),
3519
+ pane_index: Some(0), tty: Some("/dev/ttys077".into()),
3520
+ current_command: Some("pi".into()), current_path: Some(workspace.into()),
3521
+ active: true, pane_pid: None, leader_env: BTreeMap::new(),
3522
+ },
3523
+ endpoint: "/private/tmp/fixture-exec-leader.sock".into(),
3524
+ tmux: "/private/tmp/fixture-exec-leader.sock,123,0".into(),
3525
+ };
3526
+ (plan, authority)
3527
+ }
3528
+
3529
+ fn exec_binding_workspace() -> std::path::PathBuf {
3530
+ let workspace = std::env::temp_dir().join(format!("ta-exec-bootstrap-{}-{}",
3531
+ std::process::id(), std::time::SystemTime::now()
3532
+ .duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
3533
+ std::fs::create_dir(&workspace).unwrap();
3534
+ workspace
3535
+ }
3536
+
3537
+ #[test]
3538
+ fn exec_provider_non_prefixed_receiver_produces_fresh_quick_start_bootstrap() {
3539
+ let workspace = exec_binding_workspace();
3540
+ let (plan, authority) = exec_binding_fixture(&workspace);
3541
+ persist_exec_provider_leader_binding(&plan, &workspace, &authority, false).unwrap();
3542
+ let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
3543
+ let before = state.clone();
3544
+ assert!(state["session_name"].is_null());
3545
+ assert!(state["teams"]["current"]["session_name"].is_null());
3546
+ assert_eq!(state["teams"]["current"]["leader_receiver"]["session_name"], "cursor-gate-leader");
3547
+ assert!(!crate::lifecycle::launch::quick_start_has_runtime_for_test(&state, "current"));
3548
+ assert_eq!(state, before);
3549
+ std::fs::remove_dir_all(workspace).unwrap();
3550
+ }
3551
+
3552
+ #[test]
3553
+ fn exec_provider_preserves_existing_workers_and_never_clears_runtime_markers() {
3554
+ for status in ["running", "stopped", "unknown"] {
3555
+ let workspace = exec_binding_workspace();
3556
+ let (plan, authority) = exec_binding_fixture(&workspace);
3557
+ let path = crate::state::persist::runtime_state_path(&workspace);
3558
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
3559
+ std::fs::write(&path, serde_json::json!({
3560
+ "active_team_key":"current", "session_name":"team-existing-workers",
3561
+ "spec_path":"/fixture/spec.yml", "team_dir":"/fixture/roles",
3562
+ "agents":{"worker":{"agent_id":"worker","provider":"pi","status":status}},
3563
+ "tasks":[{"task_id":"existing-task"}],
3564
+ }).to_string()).unwrap();
3565
+ persist_exec_provider_leader_binding(&plan, &workspace, &authority, false).unwrap();
3566
+ let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
3567
+ for row in [&state, &state["teams"]["current"]] {
3568
+ assert_eq!(row["session_name"], "team-existing-workers");
3569
+ assert_eq!(row["agents"]["worker"]["status"], status);
3570
+ assert_eq!(row["spec_path"], "/fixture/spec.yml");
3571
+ assert_eq!(row["team_dir"], "/fixture/roles");
3572
+ assert_eq!(row["tasks"][0]["task_id"], "existing-task");
3573
+ }
3574
+ // Ownership writers update only the canonical team record, not the
3575
+ // legacy root view that retains the existing worker runtime.
3576
+ assert_eq!(
3577
+ state["teams"]["current"]["leader_receiver"]["session_name"],
3578
+ "cursor-gate-leader"
3579
+ );
3580
+ assert!(state["leader_receiver"].is_null());
3581
+ assert!(crate::lifecycle::launch::quick_start_has_runtime_for_test(&state, "current"));
3582
+ std::fs::remove_dir_all(workspace).unwrap();
3583
+ }
3584
+ }
3585
+
3368
3586
  #[test]
3369
3587
  #[serial_test::serial(env)]
3370
3588
  fn default_exec_provider_persists_current_pane_binding_before_provider_exec() {
@@ -3402,6 +3620,8 @@ mod tests {
3402
3620
  session_name: None,
3403
3621
  argv: vec!["sh".to_string(), "-c".to_string(), command],
3404
3622
  provider_argv: vec!["fake".to_string()],
3623
+ leader_prompt: None,
3624
+ session_capture: None,
3405
3625
  leader_window: None,
3406
3626
  is_external_leader: false,
3407
3627
  leader_env: BTreeMap::new(),
@@ -3931,6 +4151,8 @@ mod tests {
3931
4151
  session_name: Some(SessionName::new("team-agent-leader-claude_code-demo")),
3932
4152
  argv: vec!["tmux".to_string(), "attach-session".to_string()],
3933
4153
  provider_argv: vec!["claude".to_string()],
4154
+ leader_prompt: None,
4155
+ session_capture: None,
3934
4156
  leader_window: Some(WindowName::new("claude_code")),
3935
4157
  is_external_leader: false,
3936
4158
  leader_env: BTreeMap::new(),
@@ -3939,6 +4161,74 @@ mod tests {
3939
4161
  }
3940
4162
  }
3941
4163
 
4164
+ #[cfg(unix)]
4165
+ #[test]
4166
+ fn leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec() {
4167
+ const TEST: &str = "leader::start::tests::leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec";
4168
+ const KEY: &str = "TEAM_AGENT_TEST_PROMPT_CAPTURE_CASE";
4169
+ const BODY: &str = "SENSITIVE_PROMPT_309_CAPTURE";
4170
+ if let Ok(case) = std::env::var(KEY) {
4171
+ let root = std::env::temp_dir().join(format!("ta-prompt-capture-{}-{case}", std::process::id()));
4172
+ std::fs::create_dir_all(&root).unwrap();
4173
+ let mut plan = persist_test_plan(&root);
4174
+ plan.mode = if case == "exec_configured" { LeaderStartMode::ExecProvider } else { LeaderStartMode::NewTmuxSession };
4175
+ if case != "transport_absent" {
4176
+ plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
4177
+ config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
4178
+ });
4179
+ }
4180
+ // Controlled launcher echoes its received argv and fails; no native Agent/Team.
4181
+ let argv = vec!["/bin/sh".to_string(), "-c".to_string(),
4182
+ "printf '%s\\n' \"$1\"; printf '%s\\n' \"$1\" >&2; exit 23".to_string(), "launcher".to_string(), BODY.to_string()];
4183
+ let outcome = super::run_leader_argv(&argv, &BTreeMap::new(), &plan, &root, None).unwrap();
4184
+ assert_eq!(outcome.status.code(), Some(23));
4185
+ if plan.leader_prompt.is_some() { assert!(!outcome.stderr.contains(BODY)); }
4186
+ std::fs::remove_dir_all(root).unwrap();
4187
+ return;
4188
+ }
4189
+ for case in ["transport_configured", "transport_absent", "exec_configured"] {
4190
+ let output = std::process::Command::new(std::env::current_exe().unwrap())
4191
+ .args(["--exact", TEST, "--nocapture", "--test-threads=1"])
4192
+ .env(KEY, case).env_remove("TMUX").env_remove("TMUX_PANE")
4193
+ .output().unwrap();
4194
+ assert!(output.status.success(), "controlled child failed: {:?} {}", output.status, String::from_utf8_lossy(&output.stderr));
4195
+ for bytes in [&output.stdout, &output.stderr] {
4196
+ assert_eq!(String::from_utf8_lossy(bytes).contains(BODY), case != "transport_configured", "capture policy differs for {case}");
4197
+ }
4198
+ }
4199
+ }
4200
+
4201
+ #[test]
4202
+ fn leader_prompt_diagnostics_hide_payload_but_keep_unconfigured_baseline_and_native_cause() {
4203
+ let root = std::env::temp_dir().join(format!("ta-prompt-diagnostics-{}", std::process::id()));
4204
+ std::fs::create_dir_all(&root).unwrap();
4205
+ let path = root.join("launcher.log");
4206
+ let mut plan = persist_test_plan(&root);
4207
+ let plain = vec!["tmux".to_string(), "attach-session".to_string()];
4208
+ super::write_launcher_diagnostics_header(&path, &plan, &plain).unwrap();
4209
+ assert_eq!(std::fs::read_to_string(&path).unwrap(),
4210
+ "startup_stage=prepare\nmode=ManagedTmuxClient\nprovider=claude_code\nargv=tmux attach-session\n");
4211
+ const BODY: &str = "LEADER_ONLY_SENSITIVE_309";
4212
+ plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
4213
+ config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
4214
+ });
4215
+ let shell = vec!["tmux".to_string(), "new-session".to_string(),
4216
+ format!("claude --append-system-prompt '{BODY}'")];
4217
+ super::write_launcher_diagnostics_header(&path, &plan, &shell).unwrap();
4218
+ let header = std::fs::read_to_string(&path).unwrap();
4219
+ assert!(!header.contains(BODY));
4220
+ assert!(header.contains("leader_prompt={") && header.contains("--append-system-prompt"));
4221
+ assert!(!super::prompt_aware_diagnostic_text(&plan, BODY).contains(BODY));
4222
+ let error = super::prompt_aware_spawn_error(&plan, TransportError::Subprocess {
4223
+ argv: shell, code: Some(7), stderr: BODY.to_string(),
4224
+ });
4225
+ let crate::leader::LeaderError::Prompt(error) = error else { panic!("typed carrier failure required") };
4226
+ assert_eq!(error.reason, "leader_prompt_carrier_failed");
4227
+ assert_eq!(error.exit_code, Some(7));
4228
+ assert!(!error.report().to_string().contains(BODY));
4229
+ std::fs::remove_dir_all(root).unwrap();
4230
+ }
4231
+
3942
4232
  #[test]
3943
4233
  fn multi_leader_window_keeps_base_when_free() {
3944
4234
  let window =
@@ -4020,6 +4310,48 @@ mod tests {
4020
4310
  let _ = std::fs::remove_dir_all(&workspace);
4021
4311
  }
4022
4312
 
4313
+ #[test]
4314
+ fn pi_managed_leader_persists_native_identity_with_the_canonical_pane() {
4315
+ let workspace = std::env::temp_dir().join(format!(
4316
+ "ta-pi-leader-native-binding-{}", std::process::id()
4317
+ ));
4318
+ std::fs::create_dir_all(workspace.join(".team/runtime")).unwrap();
4319
+ let mut plan = persist_test_plan(&workspace);
4320
+ plan.provider = Provider::Pi;
4321
+ plan.session_capture = Some(crate::provider::CaptureSessionContext {
4322
+ agent_id: "leader".into(),
4323
+ spawn_cwd: workspace.clone(),
4324
+ pane_id: None,
4325
+ pane_pid: None,
4326
+ spawned_at: Some("2026-10-10T03:59:07Z".into()),
4327
+ expected_session_id: Some(crate::provider::SessionId::new("native-first")),
4328
+ provider_projects_root: Some(workspace.join("pi-sessions")),
4329
+ });
4330
+ let mut pane = SpawnResult {
4331
+ pane_id: PaneId::new("%0"),
4332
+ session: SessionName::new("team-agent-leader-pi-demo"),
4333
+ window: WindowName::new("pi"),
4334
+ child_pid: Some(1),
4335
+ };
4336
+ persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
4337
+ let first = crate::state::persist::load_runtime_state(&workspace).unwrap();
4338
+ let receiver = &first["teams"]["current"]["leader_receiver"];
4339
+ let native = &receiver["native_session"];
4340
+ assert_eq!(native["expected_session_id"], "native-first");
4341
+ assert_ne!(native["expected_session_id"], receiver["leader_session_uuid"]);
4342
+ assert_eq!(native["provider_projects_root"], serde_json::json!(workspace.join("pi-sessions")));
4343
+ for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
4344
+ assert_eq!(native[key], receiver[key]);
4345
+ }
4346
+ pane.pane_id = PaneId::new("%1");
4347
+ plan.session_capture.as_mut().unwrap().expected_session_id =
4348
+ Some(crate::provider::SessionId::new("native-second"));
4349
+ persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
4350
+ let second = crate::state::persist::load_runtime_state(&workspace).unwrap();
4351
+ assert_eq!(second["teams"]["current"]["leader_receiver"]["native_session"], *native);
4352
+ let _ = std::fs::remove_dir_all(workspace);
4353
+ }
4354
+
4023
4355
  #[test]
4024
4356
  fn multi_leader_persist_keeps_first_owner_uuid() {
4025
4357
  let workspace = std::env::temp_dir().join(format!(
@@ -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 {