@team-agent/installer 0.5.110 → 0.5.111

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.
@@ -48,10 +48,13 @@ use thiserror::Error;
48
48
  use crate::messaging::{self, MessageTarget, SendOptions, TrustedSender};
49
49
  use crate::model::ids::{TaskId, TeamKey};
50
50
 
51
- pub(crate) const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
52
- pub(crate) const QUICK_START_REMINDER: &str = "Reminder: Do not inspect raw worker pane output during normal operation. Use team-agent status / inbox instead. Wait for report_result.";
53
- pub(crate) const SEND_REMINDER: &str = "Message delivered. Wait for the worker to report_result. Do not poll the worker terminal with capture-pane.";
54
- pub(crate) const STATUS_REMINDER: &str = "Results are finalized automatically; use --watch-result or team-agent inbox AGENT -n 3 for transport fallback. Do not capture-pane worker terminals.";
51
+ pub(crate) const COMMS_BOUNDARY_TEXT: &str =
52
+ "这里只检查终端连接是否一致,不实际发送消息,也不证明队友已回复。";
53
+ pub(crate) const QUICK_START_REMINDER: &str = "下一步:向队友派发任务;用 team-agent status 看状态,用 team-agent inbox leader -n 3 看回复,不读取队友终端内容。";
54
+ pub(crate) const SEND_REMINDER: &str =
55
+ "等队友真正回复,用 team-agent inbox leader -n 3 查看;不要读取队友终端内容。";
56
+ pub(crate) const STATUS_REMINDER: &str =
57
+ "下一步:用 team-agent inbox leader -n 3 查看回复;状态不是任务完成证明,不读取队友终端内容。";
55
58
 
56
59
  pub mod adapters;
57
60
  pub mod attach_app_server_leader;
@@ -137,12 +140,7 @@ pub mod lifecycle_port {
137
140
  backend: Option<&str>,
138
141
  ) -> Result<Value, CliError> {
139
142
  match crate::lifecycle::quick_start_in_workspace_with_backend(
140
- workspace,
141
- agents_dir,
142
- name,
143
- yes,
144
- team_id,
145
- backend,
143
+ workspace, agents_dir, name, yes, team_id, backend,
146
144
  ) {
147
145
  Ok(report) => Ok(quick_start_value(report)),
148
146
  Err(e) => Ok(error_value(e)),
@@ -677,7 +675,9 @@ pub mod lifecycle_port {
677
675
  "session_marker+state_session+pane_path",
678
676
  "positive workspace/team ownership",
679
677
  ) {
680
- result.error.get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
678
+ result
679
+ .error
680
+ .get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
681
681
  } else if let Err(error) = transport.kill_session(&session) {
682
682
  if !tmux_absent_error(&error.to_string()) {
683
683
  result.error.get_or_insert_with(|| error.to_string());
@@ -691,7 +691,9 @@ pub mod lifecycle_port {
691
691
  }
692
692
  SessionOwnership::Unknown => {
693
693
  push_unique_session(&mut result.spared_sessions, session);
694
- result.error.get_or_insert_with(|| "session ownership is unknown; destructive cleanup refused".to_string());
694
+ result.error.get_or_insert_with(|| {
695
+ "session ownership is unknown; destructive cleanup refused".to_string()
696
+ });
695
697
  }
696
698
  SessionOwnership::Gone => {}
697
699
  }
@@ -718,7 +720,11 @@ pub mod lifecycle_port {
718
720
  state
719
721
  .get("teams")
720
722
  .and_then(Value::as_object)
721
- .and_then(|teams| teams.values().find_map(|team| state_for_session(team, session)))
723
+ .and_then(|teams| {
724
+ teams
725
+ .values()
726
+ .find_map(|team| state_for_session(team, session))
727
+ })
722
728
  }
723
729
 
724
730
  fn session_ownership(
@@ -759,27 +765,33 @@ pub mod lifecycle_port {
759
765
  if owner.team != expected_team {
760
766
  return SessionOwnership::Foreign;
761
767
  }
762
- let Some(expected_generation) = state_generation(identity_state)
763
- .filter(|generation| !generation.is_empty()) else {
768
+ let Some(expected_generation) =
769
+ state_generation(identity_state).filter(|generation| !generation.is_empty())
770
+ else {
764
771
  return SessionOwnership::Unknown;
765
772
  };
766
773
  if owner.generation != expected_generation {
767
774
  return SessionOwnership::Foreign;
768
775
  }
769
776
  for target in session_targets {
770
- if target.current_path.as_ref().is_some_and(|path| {
771
- path_is_under(path.as_path(), workspace)
772
- }) {
777
+ if target
778
+ .current_path
779
+ .as_ref()
780
+ .is_some_and(|path| path_is_under(path.as_path(), workspace))
781
+ {
773
782
  continue;
774
783
  }
775
784
  // An exited keepalive has no cwd or live process. Exempt only the
776
785
  // internal anchor, with native positive dead-pane proof, AFTER the
777
786
  // session workspace/team/generation ownership gates above.
778
- let dead_anchor = target.current_path.as_ref().is_none_or(|path| {
779
- path.as_os_str().is_empty()
780
- }) && target.window_name.as_ref().is_some_and(|window| {
781
- window.as_str() == crate::tmux_backend::KEEPALIVE_WINDOW_NAME
782
- }) && matches!(transport.pane_is_dead(&target.pane_id), Ok(Some(true)));
787
+ let dead_anchor = target
788
+ .current_path
789
+ .as_ref()
790
+ .is_none_or(|path| path.as_os_str().is_empty())
791
+ && target.window_name.as_ref().is_some_and(|window| {
792
+ window.as_str() == crate::tmux_backend::KEEPALIVE_WINDOW_NAME
793
+ })
794
+ && matches!(transport.pane_is_dead(&target.pane_id), Ok(Some(true)));
783
795
  if !dead_anchor {
784
796
  return SessionOwnership::Unknown;
785
797
  }
@@ -795,7 +807,12 @@ pub mod lifecycle_port {
795
807
  use crate::transport::test_support::OfflineTransport;
796
808
  use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
797
809
 
798
- fn ownership_pane(session: &SessionName, pane: &str, window: &str, cwd: Option<PathBuf>) -> PaneInfo {
810
+ fn ownership_pane(
811
+ session: &SessionName,
812
+ pane: &str,
813
+ window: &str,
814
+ cwd: Option<PathBuf>,
815
+ ) -> PaneInfo {
799
816
  PaneInfo {
800
817
  pane_id: PaneId::new(pane),
801
818
  session: session.clone(),
@@ -817,23 +834,81 @@ pub mod lifecycle_port {
817
834
  let session = SessionName::new("owned");
818
835
  let state = json!({"team_key": "owned", "generation": "g"});
819
836
  for (window, cwd, dead, team, generation, expected) in [
820
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(true), "owned", "g", SessionOwnership::Owned),
821
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(false), "owned", "g", SessionOwnership::Unknown),
822
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, None, "owned", "g", SessionOwnership::Unknown),
823
- ("foreign", None, Some(true), "owned", "g", SessionOwnership::Unknown),
824
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, Some(PathBuf::from("/foreign-workspace")), Some(true), "owned", "g", SessionOwnership::Unknown),
825
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(true), "other-team", "g", SessionOwnership::Foreign),
826
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(true), "owned", "other-generation", SessionOwnership::Foreign),
837
+ (
838
+ crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
839
+ None,
840
+ Some(true),
841
+ "owned",
842
+ "g",
843
+ SessionOwnership::Owned,
844
+ ),
845
+ (
846
+ crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
847
+ None,
848
+ Some(false),
849
+ "owned",
850
+ "g",
851
+ SessionOwnership::Unknown,
852
+ ),
853
+ (
854
+ crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
855
+ None,
856
+ None,
857
+ "owned",
858
+ "g",
859
+ SessionOwnership::Unknown,
860
+ ),
861
+ (
862
+ "foreign",
863
+ None,
864
+ Some(true),
865
+ "owned",
866
+ "g",
867
+ SessionOwnership::Unknown,
868
+ ),
869
+ (
870
+ crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
871
+ Some(PathBuf::from("/foreign-workspace")),
872
+ Some(true),
873
+ "owned",
874
+ "g",
875
+ SessionOwnership::Unknown,
876
+ ),
877
+ (
878
+ crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
879
+ None,
880
+ Some(true),
881
+ "other-team",
882
+ "g",
883
+ SessionOwnership::Foreign,
884
+ ),
885
+ (
886
+ crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
887
+ None,
888
+ Some(true),
889
+ "owned",
890
+ "other-generation",
891
+ SessionOwnership::Foreign,
892
+ ),
827
893
  ] {
828
- let mut transport = OfflineTransport::new().with_session_owner(&workspace, team, generation);
829
- if let Some(dead) = dead { transport = transport.with_pane_dead("%1", dead); }
894
+ let mut transport =
895
+ OfflineTransport::new().with_session_owner(&workspace, team, generation);
896
+ if let Some(dead) = dead {
897
+ transport = transport.with_pane_dead("%1", dead);
898
+ }
830
899
  let targets = vec![
831
900
  ownership_pane(&session, "%2", "assistant", Some(workspace.clone())),
832
901
  ownership_pane(&session, "%1", window, cwd),
833
902
  ];
834
- assert_eq!(session_ownership(&workspace, &state, &transport, &session, &targets), expected);
903
+ assert_eq!(
904
+ session_ownership(&workspace, &state, &transport, &session, &targets),
905
+ expected
906
+ );
835
907
  if expected == SessionOwnership::Foreign {
836
- assert!(!transport.calls().contains(&"pane_is_dead"), "owner gates must precede exemption");
908
+ assert!(
909
+ !transport.calls().contains(&"pane_is_dead"),
910
+ "owner gates must precede exemption"
911
+ );
837
912
  }
838
913
  assert!(!transport.calls().contains(&"kill_session"));
839
914
  }
@@ -844,7 +919,9 @@ pub mod lifecycle_port {
844
919
  #[ignore = "isolated native tmux: explicit Grok execution, no provider subscription"]
845
920
  fn dead_anchor_scoped_shutdown_reaps_worker_wrapper_and_spares_other_team() {
846
921
  let root = std::env::temp_dir().join(format!(
847
- "ta-r2-{}-{}", std::process::id(), chrono::Utc::now().timestamp_nanos_opt().unwrap(),
922
+ "ta-r2-{}-{}",
923
+ std::process::id(),
924
+ chrono::Utc::now().timestamp_nanos_opt().unwrap(),
848
925
  ));
849
926
  std::fs::create_dir(&root).unwrap();
850
927
  let root = root.canonicalize().unwrap();
@@ -854,53 +931,109 @@ pub mod lifecycle_port {
854
931
  std::fs::create_dir(&foreign_workspace).unwrap();
855
932
  let endpoint = root.join("sock");
856
933
  let transport = crate::tmux_backend::TmuxBackend::with_runner_for_tmux_endpoint(
857
- Box::new(crate::tmux_backend::RealCommandRunner), endpoint.to_str().unwrap(),
934
+ Box::new(crate::tmux_backend::RealCommandRunner),
935
+ endpoint.to_str().unwrap(),
858
936
  );
859
937
  let session = SessionName::new("round2-owned");
860
938
  let foreign = SessionName::new("round2-foreign");
861
- struct Cleanup<'a>(&'a crate::tmux_backend::TmuxBackend, [&'a SessionName; 2], &'a Path);
939
+ struct Cleanup<'a>(
940
+ &'a crate::tmux_backend::TmuxBackend,
941
+ [&'a SessionName; 2],
942
+ &'a Path,
943
+ );
862
944
  impl Drop for Cleanup<'_> {
863
945
  fn drop(&mut self) {
864
- for session in self.1 { let _ = self.0.kill_session(session); }
946
+ for session in self.1 {
947
+ let _ = self.0.kill_session(session);
948
+ }
865
949
  let _ = std::fs::remove_dir_all(self.2);
866
950
  }
867
951
  }
868
952
  let _cleanup = Cleanup(&transport, [&session, &foreign], &root);
869
953
  let env = BTreeMap::new();
870
954
  let argv = vec!["sh".to_string(), "-c".to_string(), "exit 0".to_string()];
871
- let first = transport.spawn_first_with_worker_shell_wrapper(
872
- &session, &WindowName::new("assistant"), &argv, &workspace, &env, &[], "pi",
873
- ).unwrap();
874
- transport.set_session_owner_with_generation(&session, &workspace, "round2-owned", "g").unwrap();
875
- transport.preserve_session_before_stop(&session, &[crate::transport::Target::Pane(first.pane_id.clone())]).unwrap();
955
+ let first = transport
956
+ .spawn_first_with_worker_shell_wrapper(
957
+ &session,
958
+ &WindowName::new("assistant"),
959
+ &argv,
960
+ &workspace,
961
+ &env,
962
+ &[],
963
+ "pi",
964
+ )
965
+ .unwrap();
966
+ transport
967
+ .set_session_owner_with_generation(&session, &workspace, "round2-owned", "g")
968
+ .unwrap();
969
+ transport
970
+ .preserve_session_before_stop(
971
+ &session,
972
+ &[crate::transport::Target::Pane(first.pane_id.clone())],
973
+ )
974
+ .unwrap();
876
975
  transport.kill_pane(&first.pane_id).unwrap();
877
- let worker = transport.spawn_into_with_worker_shell_wrapper(
878
- &session, &WindowName::new("assistant"), &argv, &workspace, &env, &[], "pi",
879
- ).unwrap();
880
- let foreign_worker = transport.spawn_first(
881
- &foreign, &WindowName::new("other"), &["sleep".to_string(), "60".to_string()],
882
- &foreign_workspace, &env,
883
- ).unwrap();
884
- transport.set_session_owner_with_generation(&foreign, &foreign_workspace, "round2-foreign", "foreign-g").unwrap();
976
+ let worker = transport
977
+ .spawn_into_with_worker_shell_wrapper(
978
+ &session,
979
+ &WindowName::new("assistant"),
980
+ &argv,
981
+ &workspace,
982
+ &env,
983
+ &[],
984
+ "pi",
985
+ )
986
+ .unwrap();
987
+ let foreign_worker = transport
988
+ .spawn_first(
989
+ &foreign,
990
+ &WindowName::new("other"),
991
+ &["sleep".to_string(), "60".to_string()],
992
+ &foreign_workspace,
993
+ &env,
994
+ )
995
+ .unwrap();
996
+ transport
997
+ .set_session_owner_with_generation(
998
+ &foreign,
999
+ &foreign_workspace,
1000
+ "round2-foreign",
1001
+ "foreign-g",
1002
+ )
1003
+ .unwrap();
885
1004
  let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
886
- let marker = format!("{} 0", crate::tmux_backend::worker_provider_exit_marker("pi"));
1005
+ let marker = format!(
1006
+ "{} 0",
1007
+ crate::tmux_backend::worker_provider_exit_marker("pi")
1008
+ );
887
1009
  loop {
888
1010
  // This controlled fixture prints nothing: the worker wrapper
889
1011
  // and its inert tail supply these two lines. provider_exit_status
890
1012
  // is a leader-only receipt, not a worker-wrapper observation.
891
- let captured = transport.capture(
892
- &crate::transport::Target::Pane(worker.pane_id.clone()),
893
- crate::transport::CaptureRange::Full,
894
- ).unwrap();
1013
+ let captured = transport
1014
+ .capture(
1015
+ &crate::transport::Target::Pane(worker.pane_id.clone()),
1016
+ crate::transport::CaptureRange::Full,
1017
+ )
1018
+ .unwrap();
895
1019
  if captured.text.contains(&marker)
896
- && captured.text.contains("Provider exited; this pane no longer accepts input.")
1020
+ && captured
1021
+ .text
1022
+ .contains("Provider exited; this pane no longer accepts input.")
897
1023
  {
898
1024
  break;
899
1025
  }
900
- assert!(std::time::Instant::now() < deadline, "worker must reach its inert exit wrapper: {}", captured.text);
1026
+ assert!(
1027
+ std::time::Instant::now() < deadline,
1028
+ "worker must reach its inert exit wrapper: {}",
1029
+ captured.text
1030
+ );
901
1031
  std::thread::sleep(std::time::Duration::from_millis(20));
902
1032
  }
903
- assert_eq!(transport.pane_is_dead(&worker.pane_id).unwrap(), Some(false));
1033
+ assert_eq!(
1034
+ transport.pane_is_dead(&worker.pane_id).unwrap(),
1035
+ Some(false)
1036
+ );
904
1037
  let worker_pid = worker.child_pid.unwrap();
905
1038
  assert!(crate::platform::process::pid_is_alive(worker_pid));
906
1039
  let state = json!({
@@ -914,13 +1047,21 @@ pub mod lifecycle_port {
914
1047
  }}
915
1048
  });
916
1049
  crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
917
- let result = shutdown_with_transport_and_state(&workspace, true, None, &transport, Some(state)).unwrap();
1050
+ let result =
1051
+ shutdown_with_transport_and_state(&workspace, true, None, &transport, Some(state))
1052
+ .unwrap();
918
1053
  assert_eq!(result["ok"], true, "{result}");
919
- assert_eq!(result["residuals"], json!({"sessions": [], "processes": [], "owned_files": []}), "{result}");
1054
+ assert_eq!(
1055
+ result["residuals"],
1056
+ json!({"sessions": [], "processes": [], "owned_files": []}),
1057
+ "{result}"
1058
+ );
920
1059
  assert!(!transport.has_session(&session).unwrap());
921
1060
  assert!(!crate::platform::process::pid_is_alive(worker_pid));
922
1061
  assert!(transport.has_session(&foreign).unwrap());
923
- assert!(crate::platform::process::pid_is_alive(foreign_worker.child_pid.unwrap()));
1062
+ assert!(crate::platform::process::pid_is_alive(
1063
+ foreign_worker.child_pid.unwrap()
1064
+ ));
924
1065
  }
925
1066
 
926
1067
  #[test]
@@ -931,7 +1072,13 @@ pub mod lifecycle_port {
931
1072
  });
932
1073
  let table = [(10, 1), (20, 10), (30, 10), (31, 30)]
933
1074
  .into_iter()
934
- .map(|(pid, ppid)| ProcessInfo { pid, ppid, pgid: None, session: None, command: String::new() })
1075
+ .map(|(pid, ppid)| ProcessInfo {
1076
+ pid,
1077
+ ppid,
1078
+ pgid: None,
1079
+ session: None,
1080
+ command: String::new(),
1081
+ })
935
1082
  .collect::<Vec<_>>();
936
1083
  for (endpoint, pane_pid, should_protect) in [
937
1084
  ("/tmp/s4-worker-only.sock", 20, false),
@@ -955,8 +1102,16 @@ pub mod lifecycle_port {
955
1102
  let mut protected = ShutdownProtection::default();
956
1103
  extend_protection_with_leader_panes(&mut protected, &transport, &state, &table);
957
1104
  assert_eq!(protected.pids.contains(&pane_pid), should_protect);
958
- assert_eq!(protected.pids.contains(&31), should_protect, "leader descendants");
959
- assert_eq!(protected.pids.contains(&10), should_protect, "leader's server");
1105
+ assert_eq!(
1106
+ protected.pids.contains(&31),
1107
+ should_protect,
1108
+ "leader descendants"
1109
+ );
1110
+ assert_eq!(
1111
+ protected.pids.contains(&10),
1112
+ should_protect,
1113
+ "leader's server"
1114
+ );
960
1115
  }
961
1116
  }
962
1117
 
@@ -1211,15 +1366,14 @@ pub mod lifecycle_port {
1211
1366
  // External and managed leaders both pass the same positive ownership
1212
1367
  // gate; topology only decides whether a verified session is reduced
1213
1368
  // to worker panes or removed as a whole.
1214
- let leader_anchor_ids = collect_state_leader_anchor_pane_ids(
1215
- &state,
1216
- transport.tmux_endpoint().as_deref(),
1217
- );
1369
+ let leader_anchor_ids =
1370
+ collect_state_leader_anchor_pane_ids(&state, transport.tmux_endpoint().as_deref());
1218
1371
  let (live_targets_now, live_targets_known) = match transport.list_targets() {
1219
1372
  Ok(targets) => (targets, true),
1220
1373
  Err(error) => {
1221
1374
  push_unique_session(&mut spared_sessions, session.clone());
1222
- kill_error.get_or_insert_with(|| format!("session ownership probe failed: {error}"));
1375
+ kill_error
1376
+ .get_or_insert_with(|| format!("session ownership probe failed: {error}"));
1223
1377
  (Vec::new(), false)
1224
1378
  }
1225
1379
  };
@@ -1237,7 +1391,9 @@ pub mod lifecycle_port {
1237
1391
  if ownership != SessionOwnership::Owned {
1238
1392
  push_unique_session(&mut spared_sessions, session.clone());
1239
1393
  if ownership == SessionOwnership::Unknown {
1240
- kill_error.get_or_insert_with(|| "session ownership is unknown; destructive cleanup refused".to_string());
1394
+ kill_error.get_or_insert_with(|| {
1395
+ "session ownership is unknown; destructive cleanup refused".to_string()
1396
+ });
1241
1397
  }
1242
1398
  } else {
1243
1399
  let session_has_leader_anchor = live_targets_now.iter().any(|target| {
@@ -1247,7 +1403,8 @@ pub mod lifecycle_port {
1247
1403
  if crate::state::projection::state_is_managed_leader(&state)
1248
1404
  && session_has_leader_anchor
1249
1405
  {
1250
- let _ = event_log_write_session_spared(&run_workspace, session, &leader_anchor_ids);
1406
+ let _ =
1407
+ event_log_write_session_spared(&run_workspace, session, &leader_anchor_ids);
1251
1408
  push_unique_session(&mut spared_sessions, session.clone());
1252
1409
  let worker_panes = collect_session_worker_panes(
1253
1410
  &state,
@@ -1256,7 +1413,8 @@ pub mod lifecycle_port {
1256
1413
  &live_targets_now,
1257
1414
  );
1258
1415
  let event_log = crate::event_log::EventLog::new(&run_workspace);
1259
- let identity_state = state_for_session(&state, session.as_str()).unwrap_or(&state);
1416
+ let identity_state =
1417
+ state_for_session(&state, session.as_str()).unwrap_or(&state);
1260
1418
  for pane in &worker_panes {
1261
1419
  let pane_target = vec![pane.as_str().to_string()];
1262
1420
  let audit = crate::kill_audit::pre_kill_audit_scoped(
@@ -1272,7 +1430,8 @@ pub mod lifecycle_port {
1272
1430
  "positive workspace/team ownership",
1273
1431
  );
1274
1432
  if let Err(error) = audit {
1275
- kill_error.get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
1433
+ kill_error
1434
+ .get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
1276
1435
  continue;
1277
1436
  }
1278
1437
  if let Err(error) = transport.kill_pane(pane) {
@@ -1324,7 +1483,8 @@ pub mod lifecycle_port {
1324
1483
  if team.is_none() {
1325
1484
  deadline.check("shared_socket_cleanup")?;
1326
1485
  let event_log = crate::event_log::EventLog::new(&run_workspace);
1327
- let cleanup = bare_shutdown_socket_cleanup(&run_workspace, transport, &state, &event_log);
1486
+ let cleanup =
1487
+ bare_shutdown_socket_cleanup(&run_workspace, transport, &state, &event_log);
1328
1488
  for session in cleanup.killed_sessions {
1329
1489
  push_unique_session(&mut killed_sessions, session);
1330
1490
  }
@@ -1619,10 +1779,8 @@ pub mod lifecycle_port {
1619
1779
  .and_then(Value::as_str)
1620
1780
  .filter(|value| !value.is_empty())
1621
1781
  .unwrap_or_else(|| session.as_str());
1622
- let leader_panes = collect_state_leader_anchor_pane_ids(
1623
- state,
1624
- transport.tmux_endpoint().as_deref(),
1625
- );
1782
+ let leader_panes =
1783
+ collect_state_leader_anchor_pane_ids(state, transport.tmux_endpoint().as_deref());
1626
1784
  let event_log = crate::event_log::EventLog::new(workspace);
1627
1785
  let team_key = state.get("team_key").and_then(Value::as_str);
1628
1786
  let generation = state_generation(state);
@@ -2142,10 +2300,13 @@ pub mod lifecycle_port {
2142
2300
  let spawn_cwds = state_spawn_cwds(state, scope);
2143
2301
  for root in root_pids {
2144
2302
  for pid in process_tree_from_table(*root, table) {
2145
- let owned = table
2146
- .iter()
2147
- .find(|process| process.pid == pid)
2148
- .is_some_and(|process| process_matches_workspace(process, workspace, &spawn_cwds));
2303
+ let owned =
2304
+ table
2305
+ .iter()
2306
+ .find(|process| process.pid == pid)
2307
+ .is_some_and(|process| {
2308
+ process_matches_workspace(process, workspace, &spawn_cwds)
2309
+ });
2149
2310
  if owned && !protected.contains_pid(pid) && seen.insert(pid) {
2150
2311
  pids.push(pid);
2151
2312
  }
@@ -2223,14 +2384,8 @@ pub mod lifecycle_port {
2223
2384
  let round_table = shutdown_table_snapshot(workspace, probe_degraded, "residual_round");
2224
2385
  let mut protected = shutdown_protection_set(&round_table);
2225
2386
  extend_protection_with_leader_panes(&mut protected, transport, state, &round_table);
2226
- let residuals = matched_processes(
2227
- workspace,
2228
- state,
2229
- root_pids,
2230
- &protected,
2231
- scope,
2232
- &round_table,
2233
- );
2387
+ let residuals =
2388
+ matched_processes(workspace, state, root_pids, &protected, scope, &round_table);
2234
2389
  if residuals.is_empty() {
2235
2390
  return;
2236
2391
  }
@@ -2282,7 +2437,10 @@ pub mod lifecycle_port {
2282
2437
  ) -> T {
2283
2438
  let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
2284
2439
  let previous = probes.get();
2285
- probes.set(ShutdownTestProbes { process_table: Some(probe), ..previous });
2440
+ probes.set(ShutdownTestProbes {
2441
+ process_table: Some(probe),
2442
+ ..previous
2443
+ });
2286
2444
  previous
2287
2445
  });
2288
2446
  let _restore = ShutdownTestProbeRestore(previous);
@@ -2296,7 +2454,10 @@ pub mod lifecycle_port {
2296
2454
  ) -> T {
2297
2455
  let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
2298
2456
  let previous = probes.get();
2299
- probes.set(ShutdownTestProbes { pid_is_alive: Some(probe), ..previous });
2457
+ probes.set(ShutdownTestProbes {
2458
+ pid_is_alive: Some(probe),
2459
+ ..previous
2460
+ });
2300
2461
  previous
2301
2462
  });
2302
2463
  let _restore = ShutdownTestProbeRestore(previous);
@@ -2551,17 +2712,29 @@ pub mod lifecycle_port {
2551
2712
  endpoint: Option<&str>,
2552
2713
  out: &mut std::collections::BTreeSet<String>,
2553
2714
  ) {
2554
- for (key, peer_key) in [("team_owner", "leader_receiver"), ("leader_receiver", "team_owner")] {
2555
- let Some(anchor) = state.get(key) else { continue };
2556
- let Some(pane_id) = anchor.get("pane_id").and_then(Value::as_str).filter(|s| !s.is_empty()) else {
2715
+ for (key, peer_key) in [
2716
+ ("team_owner", "leader_receiver"),
2717
+ ("leader_receiver", "team_owner"),
2718
+ ] {
2719
+ let Some(anchor) = state.get(key) else {
2720
+ continue;
2721
+ };
2722
+ let Some(pane_id) = anchor
2723
+ .get("pane_id")
2724
+ .and_then(Value::as_str)
2725
+ .filter(|s| !s.is_empty())
2726
+ else {
2557
2727
  continue;
2558
2728
  };
2559
2729
  // team_owner often records only a pane ID; its receiver supplies
2560
2730
  // the endpoint only when it describes that exact same anchor.
2561
- let anchor_endpoint = anchor.get("tmux_socket").and_then(Value::as_str)
2731
+ let anchor_endpoint = anchor
2732
+ .get("tmux_socket")
2733
+ .and_then(Value::as_str)
2562
2734
  .filter(|s| !s.is_empty())
2563
2735
  .or_else(|| {
2564
- state.get(peer_key)
2736
+ state
2737
+ .get(peer_key)
2565
2738
  .filter(|peer| peer.get("pane_id").and_then(Value::as_str) == Some(pane_id))
2566
2739
  .and_then(|peer| peer.get("tmux_socket"))
2567
2740
  .and_then(Value::as_str)
@@ -2569,7 +2742,9 @@ pub mod lifecycle_port {
2569
2742
  });
2570
2743
  if let (Some(anchor_path), Some(transport_path)) = (
2571
2744
  anchor_endpoint.and_then(socket_file_for_endpoint),
2572
- endpoint.filter(|s| !s.is_empty()).and_then(socket_file_for_endpoint),
2745
+ endpoint
2746
+ .filter(|s| !s.is_empty())
2747
+ .and_then(socket_file_for_endpoint),
2573
2748
  ) {
2574
2749
  let anchor_path = anchor_path.canonicalize().unwrap_or(anchor_path);
2575
2750
  let transport_path = transport_path.canonicalize().unwrap_or(transport_path);
@@ -2688,10 +2863,8 @@ pub mod lifecycle_port {
2688
2863
  );
2689
2864
  // Source 2: state.json team_owner / leader_receiver 真锚 pane_id(top-level +
2690
2865
  // teams[*]),per-workspace socket 命中。
2691
- let anchor_pane_ids = collect_state_leader_anchor_pane_ids(
2692
- state,
2693
- transport.tmux_endpoint().as_deref(),
2694
- );
2866
+ let anchor_pane_ids =
2867
+ collect_state_leader_anchor_pane_ids(state, transport.tmux_endpoint().as_deref());
2695
2868
  leader_pane_pids.extend(
2696
2869
  pane_targets
2697
2870
  .iter()
@@ -2707,7 +2880,8 @@ pub mod lifecycle_port {
2707
2880
  // 的 list_targets 找 anchor pane_id → pane_pid → 进入 process_tree 保护。
2708
2881
  // 不在 state 中的 socket 不查(MUST-17 不撒宽 / 不主动枚举全机器 sockets)。
2709
2882
  for socket_endpoint in collect_state_recorded_tmux_sockets(state) {
2710
- let anchor_pane_ids = collect_state_leader_anchor_pane_ids(state, Some(&socket_endpoint));
2883
+ let anchor_pane_ids =
2884
+ collect_state_leader_anchor_pane_ids(state, Some(&socket_endpoint));
2711
2885
  let cross_backend =
2712
2886
  crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket_endpoint);
2713
2887
  let cross_panes =
@@ -2784,11 +2958,7 @@ pub mod lifecycle_port {
2784
2958
  crate::platform::process::reap_child_if_possible(*pid);
2785
2959
  }
2786
2960
  let elapsed = start.elapsed();
2787
- if !pids
2788
- .iter()
2789
- .any(|pid| shutdown_pid_is_alive(*pid))
2790
- || elapsed >= timeout
2791
- {
2961
+ if !pids.iter().any(|pid| shutdown_pid_is_alive(*pid)) || elapsed >= timeout {
2792
2962
  return;
2793
2963
  }
2794
2964
  std::thread::sleep(
@@ -2805,18 +2975,13 @@ pub mod lifecycle_port {
2805
2975
  scope: ShutdownReapScope,
2806
2976
  table: &[ProcessInfo],
2807
2977
  ) -> Vec<Value> {
2808
- let mut residuals = matched_processes(
2809
- workspace, state, root_pids, protected, scope, table,
2810
- );
2978
+ let mut residuals = matched_processes(workspace, state, root_pids, protected, scope, table);
2811
2979
  let mut seen = residuals
2812
2980
  .iter()
2813
2981
  .map(|process| process.pid)
2814
2982
  .collect::<std::collections::BTreeSet<_>>();
2815
2983
  for pid in root_pids {
2816
- if !protected.contains_pid(*pid)
2817
- && shutdown_pid_is_alive(*pid)
2818
- && seen.insert(*pid)
2819
- {
2984
+ if !protected.contains_pid(*pid) && shutdown_pid_is_alive(*pid) && seen.insert(*pid) {
2820
2985
  residuals.push(ProcessInfo {
2821
2986
  pid: *pid,
2822
2987
  ppid: 0,
@@ -2966,7 +3131,11 @@ pub mod lifecycle_port {
2966
3131
  let _ = force; // Never grants authority to stop an already-running seat.
2967
3132
  let agent_id = crate::model::ids::AgentId::new(agent);
2968
3133
  match crate::lifecycle::role_config::start_agent_from_role(
2969
- workspace, &agent_id, role_config, allow_fresh, team,
3134
+ workspace,
3135
+ &agent_id,
3136
+ role_config,
3137
+ allow_fresh,
3138
+ team,
2970
3139
  ) {
2971
3140
  Ok(crate::lifecycle::StartAgentOutcome::Running {
2972
3141
  env,
@@ -2995,14 +3164,19 @@ pub mod lifecycle_port {
2995
3164
  }),
2996
3165
  )),
2997
3166
  Ok(crate::lifecycle::StartAgentOutcome::Noop { env, target }) => {
2998
- Ok(agent_action_value(agent, workspace, team, json!({
2999
- "ok": true,
3000
- "agent_id": env.agent_id.as_str(),
3001
- "status": "already_running",
3002
- "target": target,
3003
- "coordinator_started": env.coordinator_started,
3004
- "state_file": env.state_file.to_string_lossy(),
3005
- })))
3167
+ Ok(agent_action_value(
3168
+ agent,
3169
+ workspace,
3170
+ team,
3171
+ json!({
3172
+ "ok": true,
3173
+ "agent_id": env.agent_id.as_str(),
3174
+ "status": "already_running",
3175
+ "target": target,
3176
+ "coordinator_started": env.coordinator_started,
3177
+ "state_file": env.state_file.to_string_lossy(),
3178
+ }),
3179
+ ))
3006
3180
  }
3007
3181
  Ok(crate::lifecycle::StartAgentOutcome::Paused { agent_id }) => Ok(json!({
3008
3182
  "ok": false,
@@ -3035,13 +3209,7 @@ pub mod lifecycle_port {
3035
3209
  team: Option<&str>,
3036
3210
  ) -> Result<Value, CliError> {
3037
3211
  let agent_id = crate::model::ids::AgentId::new(agent);
3038
- match crate::lifecycle::reset_agent(
3039
- workspace,
3040
- &agent_id,
3041
- discard_session,
3042
- true,
3043
- team,
3044
- ) {
3212
+ match crate::lifecycle::reset_agent(workspace, &agent_id, discard_session, true, team) {
3045
3213
  Ok(crate::lifecycle::ResetAgentOutcome::Reset {
3046
3214
  env,
3047
3215
  start_mode,
@@ -3089,7 +3257,12 @@ pub mod lifecycle_port {
3089
3257
  let agent_id = crate::model::ids::AgentId::new(agent);
3090
3258
  let source = (!role_file.is_empty()).then(|| Path::new(role_file));
3091
3259
  match crate::lifecycle::role_config::add_agent_from_role(
3092
- workspace, &agent_id, source, role_config, force, team,
3260
+ workspace,
3261
+ &agent_id,
3262
+ source,
3263
+ role_config,
3264
+ force,
3265
+ team,
3093
3266
  ) {
3094
3267
  Ok(report) => Ok(agent_action_value(
3095
3268
  agent,
@@ -3108,7 +3281,12 @@ pub mod lifecycle_port {
3108
3281
  Err(e) => Ok(error_value(e)),
3109
3282
  }
3110
3283
  }
3111
- fn agent_action_value(agent: &str, workspace: &Path, team: Option<&str>, mut value: Value) -> Value {
3284
+ fn agent_action_value(
3285
+ agent: &str,
3286
+ workspace: &Path,
3287
+ team: Option<&str>,
3288
+ mut value: Value,
3289
+ ) -> Value {
3112
3290
  if value.get("ok").and_then(Value::as_bool) == Some(true) {
3113
3291
  if let Some(object) = value.as_object_mut() {
3114
3292
  if let Some(command) = super::adapters::send_command(agent, workspace, team) {
@@ -3749,10 +3927,7 @@ pub mod lifecycle_port {
3749
3927
  let mut default = detail.clone();
3750
3928
  compact_quick_start_value(&mut default);
3751
3929
  assert_eq!(default["status"], json!("leader_bind_refused"));
3752
- assert_eq!(
3753
- default["reason"],
3754
- json!("provider_unresolved")
3755
- );
3930
+ assert_eq!(default["reason"], json!("provider_unresolved"));
3756
3931
  assert_eq!(
3757
3932
  default["next_actions"],
3758
3933
  json!(["set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"])
@@ -3772,14 +3947,18 @@ pub mod lifecycle_port {
3772
3947
  );
3773
3948
  assert!(default["readiness"].get("all_spawned").is_none());
3774
3949
  assert!(default["readiness"].get("all_attached_receiver").is_none());
3775
- assert!(default["readiness"].get("leader_receiver_attached").is_none());
3950
+ assert!(default["readiness"]
3951
+ .get("leader_receiver_attached")
3952
+ .is_none());
3776
3953
  assert_eq!(
3777
3954
  default["worker_readiness"]["all_workers_spawned"],
3778
3955
  json!(true)
3779
3956
  );
3780
3957
  assert!(default["worker_readiness"].get("all_spawned").is_none());
3781
3958
  assert!(detail["readiness"].get("all_spawned").is_some());
3782
- assert!(detail["worker_readiness"].get("leader_receiver_attached").is_some());
3959
+ assert!(detail["worker_readiness"]
3960
+ .get("leader_receiver_attached")
3961
+ .is_some());
3783
3962
  }
3784
3963
 
3785
3964
  #[test]
@@ -3935,7 +4114,10 @@ pub mod lifecycle_port {
3935
4114
  compact_restart_value(&mut default);
3936
4115
  assert_eq!(default["status"], json!("partial"));
3937
4116
  assert_eq!(default["reason"], json!("restart_agent_failed"));
3938
- assert_eq!(default["next_actions"], json!(["reset-agent worker --discard-session"]));
4117
+ assert_eq!(
4118
+ default["next_actions"],
4119
+ json!(["reset-agent worker --discard-session"])
4120
+ );
3939
4121
  assert!(default.get("coordinator").is_none());
3940
4122
  assert!(detail.get("coordinator").is_some());
3941
4123
  }
@@ -3975,7 +4157,9 @@ pub mod lifecycle_port {
3975
4157
  &transport,
3976
4158
  )
3977
4159
  .expect("first quick-start");
3978
- let crate::lifecycle::QuickStartReport::Ready { team: ready_team, .. } = &first
4160
+ let crate::lifecycle::QuickStartReport::Ready {
4161
+ team: ready_team, ..
4162
+ } = &first
3979
4163
  else {
3980
4164
  panic!("first producer must return Ready; got {first:?}");
3981
4165
  };
@@ -3992,7 +4176,7 @@ pub mod lifecycle_port {
3992
4176
  "team-agent".to_string(),
3993
4177
  "send".to_string(),
3994
4178
  "implementer".to_string(),
3995
- "MESSAGE".to_string(),
4179
+ "请完成任务并把答案回复给 leader。".to_string(),
3996
4180
  "--workspace".to_string(),
3997
4181
  team_dir.to_string_lossy().into_owned(),
3998
4182
  "--team".to_string(),
@@ -4029,7 +4213,7 @@ pub mod lifecycle_port {
4029
4213
  "team-agent".to_string(),
4030
4214
  "send".to_string(),
4031
4215
  "implementer".to_string(),
4032
- "MESSAGE".to_string(),
4216
+ "请完成任务并把答案回复给 leader。".to_string(),
4033
4217
  "--workspace".to_string(),
4034
4218
  team_dir.to_string_lossy().into_owned(),
4035
4219
  "--team".to_string(),
@@ -4054,10 +4238,7 @@ pub mod lifecycle_port {
4054
4238
  value
4055
4239
  .get("agent_ids")
4056
4240
  .and_then(Value::as_array)
4057
- .map(|items| items
4058
- .iter()
4059
- .filter_map(Value::as_str)
4060
- .collect::<Vec<_>>()),
4241
+ .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
4061
4242
  Some(vec!["sol", "luna"])
4062
4243
  );
4063
4244
  }
@@ -4087,14 +4268,15 @@ pub mod lifecycle_port {
4087
4268
 
4088
4269
  #[test]
4089
4270
  fn existing_runtime_send_guidance_does_not_pair_alice_with_bob() {
4090
- let mut value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
4091
- team: Some("bob".to_string()),
4092
- session_name: Some(crate::transport::SessionName::new("team-bob")),
4093
- state_path: Some(PathBuf::from("/tmp/state.json")),
4094
- next_actions: vec!["restart".to_string()],
4095
- attach_commands: Vec::new(),
4096
- agent_ids: vec!["bob".to_string()],
4097
- });
4271
+ let mut value =
4272
+ quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
4273
+ team: Some("bob".to_string()),
4274
+ session_name: Some(crate::transport::SessionName::new("team-bob")),
4275
+ state_path: Some(PathBuf::from("/tmp/state.json")),
4276
+ next_actions: vec!["restart".to_string()],
4277
+ attach_commands: Vec::new(),
4278
+ agent_ids: vec!["bob".to_string()],
4279
+ });
4098
4280
  crate::cli::adapters::append_send_guidance(
4099
4281
  &mut value,
4100
4282
  Path::new("/tmp/ws"),
@@ -4189,7 +4371,7 @@ pub mod lifecycle_port {
4189
4371
  value["reason"] = json!(reason);
4190
4372
  }
4191
4373
  value
4192
- },
4374
+ }
4193
4375
  crate::lifecycle::RestartReport::Partial {
4194
4376
  session_name,
4195
4377
  agents,
@@ -4243,7 +4425,7 @@ pub mod lifecycle_port {
4243
4425
  value["leader_bind_reason"] = json!(reason);
4244
4426
  }
4245
4427
  value
4246
- },
4428
+ }
4247
4429
  crate::lifecycle::RestartReport::Failed {
4248
4430
  session_name,
4249
4431
  failed_agents,
@@ -5254,7 +5436,11 @@ pub mod diagnose_port {
5254
5436
  ///
5255
5437
  /// `orphan_gate(fix, confirm)`(`--gate orphans`)。CI gate。
5256
5438
  #[cfg(test)]
5257
- pub(super) fn orphan_gate(workspace: &Path, fix: bool, confirm: bool) -> Result<Value, CliError> {
5439
+ pub(super) fn orphan_gate(
5440
+ workspace: &Path,
5441
+ fix: bool,
5442
+ confirm: bool,
5443
+ ) -> Result<Value, CliError> {
5258
5444
  crate::diagnose::orphans::orphan_gate_json(workspace, fix, confirm)
5259
5445
  }
5260
5446
  ///