@team-agent/installer 0.5.109 → 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.
Files changed (34) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +33 -0
  4. package/README.zh.md +30 -0
  5. package/crates/team-agent/src/cli/adapters.rs +234 -73
  6. package/crates/team-agent/src/cli/emit.rs +494 -201
  7. package/crates/team-agent/src/cli/leader.rs +4 -9
  8. package/crates/team-agent/src/cli/leaders.rs +90 -29
  9. package/crates/team-agent/src/cli/mod.rs +348 -161
  10. package/crates/team-agent/src/cli/models.rs +80 -24
  11. package/crates/team-agent/src/cli/named_address.rs +114 -25
  12. package/crates/team-agent/src/cli/route.rs +212 -0
  13. package/crates/team-agent/src/cli/send/presentation.rs +60 -41
  14. package/crates/team-agent/src/cli/send/resolve.rs +25 -18
  15. package/crates/team-agent/src/cli/send.rs +4 -4
  16. package/crates/team-agent/src/cli/spec.rs +56 -106
  17. package/crates/team-agent/src/cli/tests/base.rs +189 -82
  18. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
  19. package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
  20. package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
  21. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
  22. package/crates/team-agent/src/cli/types.rs +22 -13
  23. package/crates/team-agent/src/leader/registry.rs +3 -3
  24. package/crates/team-agent/src/leader/start.rs +5 -1
  25. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -0
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
  27. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +1457 -0
  28. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
  29. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
  30. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  31. package/crates/team-agent/src/provider/argv_route.rs +409 -0
  32. package/crates/team-agent/src/provider/mod.rs +3 -2
  33. package/package.json +4 -4
  34. package/skills/team-agent/SKILL.md +32 -0
@@ -48,21 +48,25 @@ 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;
58
61
  pub mod diagnose;
59
62
  pub mod emit;
60
63
  pub mod helpers;
61
- pub mod models;
62
64
  pub mod leader;
63
65
  pub mod leaders;
66
+ pub mod models;
64
67
  pub mod named_address;
65
68
  pub mod profile;
69
+ pub(crate) mod route;
66
70
  pub mod send;
67
71
  pub(crate) mod spec;
68
72
  pub mod status;
@@ -136,12 +140,7 @@ pub mod lifecycle_port {
136
140
  backend: Option<&str>,
137
141
  ) -> Result<Value, CliError> {
138
142
  match crate::lifecycle::quick_start_in_workspace_with_backend(
139
- workspace,
140
- agents_dir,
141
- name,
142
- yes,
143
- team_id,
144
- backend,
143
+ workspace, agents_dir, name, yes, team_id, backend,
145
144
  ) {
146
145
  Ok(report) => Ok(quick_start_value(report)),
147
146
  Err(e) => Ok(error_value(e)),
@@ -676,7 +675,9 @@ pub mod lifecycle_port {
676
675
  "session_marker+state_session+pane_path",
677
676
  "positive workspace/team ownership",
678
677
  ) {
679
- 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}"));
680
681
  } else if let Err(error) = transport.kill_session(&session) {
681
682
  if !tmux_absent_error(&error.to_string()) {
682
683
  result.error.get_or_insert_with(|| error.to_string());
@@ -690,7 +691,9 @@ pub mod lifecycle_port {
690
691
  }
691
692
  SessionOwnership::Unknown => {
692
693
  push_unique_session(&mut result.spared_sessions, session);
693
- 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
+ });
694
697
  }
695
698
  SessionOwnership::Gone => {}
696
699
  }
@@ -717,7 +720,11 @@ pub mod lifecycle_port {
717
720
  state
718
721
  .get("teams")
719
722
  .and_then(Value::as_object)
720
- .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
+ })
721
728
  }
722
729
 
723
730
  fn session_ownership(
@@ -758,27 +765,33 @@ pub mod lifecycle_port {
758
765
  if owner.team != expected_team {
759
766
  return SessionOwnership::Foreign;
760
767
  }
761
- let Some(expected_generation) = state_generation(identity_state)
762
- .filter(|generation| !generation.is_empty()) else {
768
+ let Some(expected_generation) =
769
+ state_generation(identity_state).filter(|generation| !generation.is_empty())
770
+ else {
763
771
  return SessionOwnership::Unknown;
764
772
  };
765
773
  if owner.generation != expected_generation {
766
774
  return SessionOwnership::Foreign;
767
775
  }
768
776
  for target in session_targets {
769
- if target.current_path.as_ref().is_some_and(|path| {
770
- path_is_under(path.as_path(), workspace)
771
- }) {
777
+ if target
778
+ .current_path
779
+ .as_ref()
780
+ .is_some_and(|path| path_is_under(path.as_path(), workspace))
781
+ {
772
782
  continue;
773
783
  }
774
784
  // An exited keepalive has no cwd or live process. Exempt only the
775
785
  // internal anchor, with native positive dead-pane proof, AFTER the
776
786
  // session workspace/team/generation ownership gates above.
777
- let dead_anchor = target.current_path.as_ref().is_none_or(|path| {
778
- path.as_os_str().is_empty()
779
- }) && target.window_name.as_ref().is_some_and(|window| {
780
- window.as_str() == crate::tmux_backend::KEEPALIVE_WINDOW_NAME
781
- }) && 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)));
782
795
  if !dead_anchor {
783
796
  return SessionOwnership::Unknown;
784
797
  }
@@ -794,7 +807,12 @@ pub mod lifecycle_port {
794
807
  use crate::transport::test_support::OfflineTransport;
795
808
  use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
796
809
 
797
- 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 {
798
816
  PaneInfo {
799
817
  pane_id: PaneId::new(pane),
800
818
  session: session.clone(),
@@ -816,23 +834,81 @@ pub mod lifecycle_port {
816
834
  let session = SessionName::new("owned");
817
835
  let state = json!({"team_key": "owned", "generation": "g"});
818
836
  for (window, cwd, dead, team, generation, expected) in [
819
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(true), "owned", "g", SessionOwnership::Owned),
820
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(false), "owned", "g", SessionOwnership::Unknown),
821
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, None, "owned", "g", SessionOwnership::Unknown),
822
- ("foreign", None, Some(true), "owned", "g", SessionOwnership::Unknown),
823
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, Some(PathBuf::from("/foreign-workspace")), Some(true), "owned", "g", SessionOwnership::Unknown),
824
- (crate::tmux_backend::KEEPALIVE_WINDOW_NAME, None, Some(true), "other-team", "g", SessionOwnership::Foreign),
825
- (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
+ ),
826
893
  ] {
827
- let mut transport = OfflineTransport::new().with_session_owner(&workspace, team, generation);
828
- 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
+ }
829
899
  let targets = vec![
830
900
  ownership_pane(&session, "%2", "assistant", Some(workspace.clone())),
831
901
  ownership_pane(&session, "%1", window, cwd),
832
902
  ];
833
- assert_eq!(session_ownership(&workspace, &state, &transport, &session, &targets), expected);
903
+ assert_eq!(
904
+ session_ownership(&workspace, &state, &transport, &session, &targets),
905
+ expected
906
+ );
834
907
  if expected == SessionOwnership::Foreign {
835
- 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
+ );
836
912
  }
837
913
  assert!(!transport.calls().contains(&"kill_session"));
838
914
  }
@@ -843,7 +919,9 @@ pub mod lifecycle_port {
843
919
  #[ignore = "isolated native tmux: explicit Grok execution, no provider subscription"]
844
920
  fn dead_anchor_scoped_shutdown_reaps_worker_wrapper_and_spares_other_team() {
845
921
  let root = std::env::temp_dir().join(format!(
846
- "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(),
847
925
  ));
848
926
  std::fs::create_dir(&root).unwrap();
849
927
  let root = root.canonicalize().unwrap();
@@ -853,53 +931,109 @@ pub mod lifecycle_port {
853
931
  std::fs::create_dir(&foreign_workspace).unwrap();
854
932
  let endpoint = root.join("sock");
855
933
  let transport = crate::tmux_backend::TmuxBackend::with_runner_for_tmux_endpoint(
856
- Box::new(crate::tmux_backend::RealCommandRunner), endpoint.to_str().unwrap(),
934
+ Box::new(crate::tmux_backend::RealCommandRunner),
935
+ endpoint.to_str().unwrap(),
857
936
  );
858
937
  let session = SessionName::new("round2-owned");
859
938
  let foreign = SessionName::new("round2-foreign");
860
- 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
+ );
861
944
  impl Drop for Cleanup<'_> {
862
945
  fn drop(&mut self) {
863
- 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
+ }
864
949
  let _ = std::fs::remove_dir_all(self.2);
865
950
  }
866
951
  }
867
952
  let _cleanup = Cleanup(&transport, [&session, &foreign], &root);
868
953
  let env = BTreeMap::new();
869
954
  let argv = vec!["sh".to_string(), "-c".to_string(), "exit 0".to_string()];
870
- let first = transport.spawn_first_with_worker_shell_wrapper(
871
- &session, &WindowName::new("assistant"), &argv, &workspace, &env, &[], "pi",
872
- ).unwrap();
873
- transport.set_session_owner_with_generation(&session, &workspace, "round2-owned", "g").unwrap();
874
- 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();
875
975
  transport.kill_pane(&first.pane_id).unwrap();
876
- let worker = transport.spawn_into_with_worker_shell_wrapper(
877
- &session, &WindowName::new("assistant"), &argv, &workspace, &env, &[], "pi",
878
- ).unwrap();
879
- let foreign_worker = transport.spawn_first(
880
- &foreign, &WindowName::new("other"), &["sleep".to_string(), "60".to_string()],
881
- &foreign_workspace, &env,
882
- ).unwrap();
883
- 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();
884
1004
  let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
885
- 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
+ );
886
1009
  loop {
887
1010
  // This controlled fixture prints nothing: the worker wrapper
888
1011
  // and its inert tail supply these two lines. provider_exit_status
889
1012
  // is a leader-only receipt, not a worker-wrapper observation.
890
- let captured = transport.capture(
891
- &crate::transport::Target::Pane(worker.pane_id.clone()),
892
- crate::transport::CaptureRange::Full,
893
- ).unwrap();
1013
+ let captured = transport
1014
+ .capture(
1015
+ &crate::transport::Target::Pane(worker.pane_id.clone()),
1016
+ crate::transport::CaptureRange::Full,
1017
+ )
1018
+ .unwrap();
894
1019
  if captured.text.contains(&marker)
895
- && 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.")
896
1023
  {
897
1024
  break;
898
1025
  }
899
- 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
+ );
900
1031
  std::thread::sleep(std::time::Duration::from_millis(20));
901
1032
  }
902
- 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
+ );
903
1037
  let worker_pid = worker.child_pid.unwrap();
904
1038
  assert!(crate::platform::process::pid_is_alive(worker_pid));
905
1039
  let state = json!({
@@ -913,13 +1047,21 @@ pub mod lifecycle_port {
913
1047
  }}
914
1048
  });
915
1049
  crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
916
- 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();
917
1053
  assert_eq!(result["ok"], true, "{result}");
918
- 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
+ );
919
1059
  assert!(!transport.has_session(&session).unwrap());
920
1060
  assert!(!crate::platform::process::pid_is_alive(worker_pid));
921
1061
  assert!(transport.has_session(&foreign).unwrap());
922
- 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
+ ));
923
1065
  }
924
1066
 
925
1067
  #[test]
@@ -930,7 +1072,13 @@ pub mod lifecycle_port {
930
1072
  });
931
1073
  let table = [(10, 1), (20, 10), (30, 10), (31, 30)]
932
1074
  .into_iter()
933
- .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
+ })
934
1082
  .collect::<Vec<_>>();
935
1083
  for (endpoint, pane_pid, should_protect) in [
936
1084
  ("/tmp/s4-worker-only.sock", 20, false),
@@ -954,8 +1102,16 @@ pub mod lifecycle_port {
954
1102
  let mut protected = ShutdownProtection::default();
955
1103
  extend_protection_with_leader_panes(&mut protected, &transport, &state, &table);
956
1104
  assert_eq!(protected.pids.contains(&pane_pid), should_protect);
957
- assert_eq!(protected.pids.contains(&31), should_protect, "leader descendants");
958
- 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
+ );
959
1115
  }
960
1116
  }
961
1117
 
@@ -1210,15 +1366,14 @@ pub mod lifecycle_port {
1210
1366
  // External and managed leaders both pass the same positive ownership
1211
1367
  // gate; topology only decides whether a verified session is reduced
1212
1368
  // to worker panes or removed as a whole.
1213
- let leader_anchor_ids = collect_state_leader_anchor_pane_ids(
1214
- &state,
1215
- transport.tmux_endpoint().as_deref(),
1216
- );
1369
+ let leader_anchor_ids =
1370
+ collect_state_leader_anchor_pane_ids(&state, transport.tmux_endpoint().as_deref());
1217
1371
  let (live_targets_now, live_targets_known) = match transport.list_targets() {
1218
1372
  Ok(targets) => (targets, true),
1219
1373
  Err(error) => {
1220
1374
  push_unique_session(&mut spared_sessions, session.clone());
1221
- 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}"));
1222
1377
  (Vec::new(), false)
1223
1378
  }
1224
1379
  };
@@ -1236,7 +1391,9 @@ pub mod lifecycle_port {
1236
1391
  if ownership != SessionOwnership::Owned {
1237
1392
  push_unique_session(&mut spared_sessions, session.clone());
1238
1393
  if ownership == SessionOwnership::Unknown {
1239
- 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
+ });
1240
1397
  }
1241
1398
  } else {
1242
1399
  let session_has_leader_anchor = live_targets_now.iter().any(|target| {
@@ -1246,7 +1403,8 @@ pub mod lifecycle_port {
1246
1403
  if crate::state::projection::state_is_managed_leader(&state)
1247
1404
  && session_has_leader_anchor
1248
1405
  {
1249
- 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);
1250
1408
  push_unique_session(&mut spared_sessions, session.clone());
1251
1409
  let worker_panes = collect_session_worker_panes(
1252
1410
  &state,
@@ -1255,7 +1413,8 @@ pub mod lifecycle_port {
1255
1413
  &live_targets_now,
1256
1414
  );
1257
1415
  let event_log = crate::event_log::EventLog::new(&run_workspace);
1258
- 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);
1259
1418
  for pane in &worker_panes {
1260
1419
  let pane_target = vec![pane.as_str().to_string()];
1261
1420
  let audit = crate::kill_audit::pre_kill_audit_scoped(
@@ -1271,7 +1430,8 @@ pub mod lifecycle_port {
1271
1430
  "positive workspace/team ownership",
1272
1431
  );
1273
1432
  if let Err(error) = audit {
1274
- 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}"));
1275
1435
  continue;
1276
1436
  }
1277
1437
  if let Err(error) = transport.kill_pane(pane) {
@@ -1323,7 +1483,8 @@ pub mod lifecycle_port {
1323
1483
  if team.is_none() {
1324
1484
  deadline.check("shared_socket_cleanup")?;
1325
1485
  let event_log = crate::event_log::EventLog::new(&run_workspace);
1326
- 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);
1327
1488
  for session in cleanup.killed_sessions {
1328
1489
  push_unique_session(&mut killed_sessions, session);
1329
1490
  }
@@ -1618,10 +1779,8 @@ pub mod lifecycle_port {
1618
1779
  .and_then(Value::as_str)
1619
1780
  .filter(|value| !value.is_empty())
1620
1781
  .unwrap_or_else(|| session.as_str());
1621
- let leader_panes = collect_state_leader_anchor_pane_ids(
1622
- state,
1623
- transport.tmux_endpoint().as_deref(),
1624
- );
1782
+ let leader_panes =
1783
+ collect_state_leader_anchor_pane_ids(state, transport.tmux_endpoint().as_deref());
1625
1784
  let event_log = crate::event_log::EventLog::new(workspace);
1626
1785
  let team_key = state.get("team_key").and_then(Value::as_str);
1627
1786
  let generation = state_generation(state);
@@ -2141,10 +2300,13 @@ pub mod lifecycle_port {
2141
2300
  let spawn_cwds = state_spawn_cwds(state, scope);
2142
2301
  for root in root_pids {
2143
2302
  for pid in process_tree_from_table(*root, table) {
2144
- let owned = table
2145
- .iter()
2146
- .find(|process| process.pid == pid)
2147
- .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
+ });
2148
2310
  if owned && !protected.contains_pid(pid) && seen.insert(pid) {
2149
2311
  pids.push(pid);
2150
2312
  }
@@ -2222,14 +2384,8 @@ pub mod lifecycle_port {
2222
2384
  let round_table = shutdown_table_snapshot(workspace, probe_degraded, "residual_round");
2223
2385
  let mut protected = shutdown_protection_set(&round_table);
2224
2386
  extend_protection_with_leader_panes(&mut protected, transport, state, &round_table);
2225
- let residuals = matched_processes(
2226
- workspace,
2227
- state,
2228
- root_pids,
2229
- &protected,
2230
- scope,
2231
- &round_table,
2232
- );
2387
+ let residuals =
2388
+ matched_processes(workspace, state, root_pids, &protected, scope, &round_table);
2233
2389
  if residuals.is_empty() {
2234
2390
  return;
2235
2391
  }
@@ -2281,7 +2437,10 @@ pub mod lifecycle_port {
2281
2437
  ) -> T {
2282
2438
  let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
2283
2439
  let previous = probes.get();
2284
- probes.set(ShutdownTestProbes { process_table: Some(probe), ..previous });
2440
+ probes.set(ShutdownTestProbes {
2441
+ process_table: Some(probe),
2442
+ ..previous
2443
+ });
2285
2444
  previous
2286
2445
  });
2287
2446
  let _restore = ShutdownTestProbeRestore(previous);
@@ -2295,7 +2454,10 @@ pub mod lifecycle_port {
2295
2454
  ) -> T {
2296
2455
  let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
2297
2456
  let previous = probes.get();
2298
- probes.set(ShutdownTestProbes { pid_is_alive: Some(probe), ..previous });
2457
+ probes.set(ShutdownTestProbes {
2458
+ pid_is_alive: Some(probe),
2459
+ ..previous
2460
+ });
2299
2461
  previous
2300
2462
  });
2301
2463
  let _restore = ShutdownTestProbeRestore(previous);
@@ -2550,17 +2712,29 @@ pub mod lifecycle_port {
2550
2712
  endpoint: Option<&str>,
2551
2713
  out: &mut std::collections::BTreeSet<String>,
2552
2714
  ) {
2553
- for (key, peer_key) in [("team_owner", "leader_receiver"), ("leader_receiver", "team_owner")] {
2554
- let Some(anchor) = state.get(key) else { continue };
2555
- 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 {
2556
2727
  continue;
2557
2728
  };
2558
2729
  // team_owner often records only a pane ID; its receiver supplies
2559
2730
  // the endpoint only when it describes that exact same anchor.
2560
- 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)
2561
2734
  .filter(|s| !s.is_empty())
2562
2735
  .or_else(|| {
2563
- state.get(peer_key)
2736
+ state
2737
+ .get(peer_key)
2564
2738
  .filter(|peer| peer.get("pane_id").and_then(Value::as_str) == Some(pane_id))
2565
2739
  .and_then(|peer| peer.get("tmux_socket"))
2566
2740
  .and_then(Value::as_str)
@@ -2568,7 +2742,9 @@ pub mod lifecycle_port {
2568
2742
  });
2569
2743
  if let (Some(anchor_path), Some(transport_path)) = (
2570
2744
  anchor_endpoint.and_then(socket_file_for_endpoint),
2571
- 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),
2572
2748
  ) {
2573
2749
  let anchor_path = anchor_path.canonicalize().unwrap_or(anchor_path);
2574
2750
  let transport_path = transport_path.canonicalize().unwrap_or(transport_path);
@@ -2687,10 +2863,8 @@ pub mod lifecycle_port {
2687
2863
  );
2688
2864
  // Source 2: state.json team_owner / leader_receiver 真锚 pane_id(top-level +
2689
2865
  // teams[*]),per-workspace socket 命中。
2690
- let anchor_pane_ids = collect_state_leader_anchor_pane_ids(
2691
- state,
2692
- transport.tmux_endpoint().as_deref(),
2693
- );
2866
+ let anchor_pane_ids =
2867
+ collect_state_leader_anchor_pane_ids(state, transport.tmux_endpoint().as_deref());
2694
2868
  leader_pane_pids.extend(
2695
2869
  pane_targets
2696
2870
  .iter()
@@ -2706,7 +2880,8 @@ pub mod lifecycle_port {
2706
2880
  // 的 list_targets 找 anchor pane_id → pane_pid → 进入 process_tree 保护。
2707
2881
  // 不在 state 中的 socket 不查(MUST-17 不撒宽 / 不主动枚举全机器 sockets)。
2708
2882
  for socket_endpoint in collect_state_recorded_tmux_sockets(state) {
2709
- 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));
2710
2885
  let cross_backend =
2711
2886
  crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket_endpoint);
2712
2887
  let cross_panes =
@@ -2783,11 +2958,7 @@ pub mod lifecycle_port {
2783
2958
  crate::platform::process::reap_child_if_possible(*pid);
2784
2959
  }
2785
2960
  let elapsed = start.elapsed();
2786
- if !pids
2787
- .iter()
2788
- .any(|pid| shutdown_pid_is_alive(*pid))
2789
- || elapsed >= timeout
2790
- {
2961
+ if !pids.iter().any(|pid| shutdown_pid_is_alive(*pid)) || elapsed >= timeout {
2791
2962
  return;
2792
2963
  }
2793
2964
  std::thread::sleep(
@@ -2804,18 +2975,13 @@ pub mod lifecycle_port {
2804
2975
  scope: ShutdownReapScope,
2805
2976
  table: &[ProcessInfo],
2806
2977
  ) -> Vec<Value> {
2807
- let mut residuals = matched_processes(
2808
- workspace, state, root_pids, protected, scope, table,
2809
- );
2978
+ let mut residuals = matched_processes(workspace, state, root_pids, protected, scope, table);
2810
2979
  let mut seen = residuals
2811
2980
  .iter()
2812
2981
  .map(|process| process.pid)
2813
2982
  .collect::<std::collections::BTreeSet<_>>();
2814
2983
  for pid in root_pids {
2815
- if !protected.contains_pid(*pid)
2816
- && shutdown_pid_is_alive(*pid)
2817
- && seen.insert(*pid)
2818
- {
2984
+ if !protected.contains_pid(*pid) && shutdown_pid_is_alive(*pid) && seen.insert(*pid) {
2819
2985
  residuals.push(ProcessInfo {
2820
2986
  pid: *pid,
2821
2987
  ppid: 0,
@@ -2965,7 +3131,11 @@ pub mod lifecycle_port {
2965
3131
  let _ = force; // Never grants authority to stop an already-running seat.
2966
3132
  let agent_id = crate::model::ids::AgentId::new(agent);
2967
3133
  match crate::lifecycle::role_config::start_agent_from_role(
2968
- workspace, &agent_id, role_config, allow_fresh, team,
3134
+ workspace,
3135
+ &agent_id,
3136
+ role_config,
3137
+ allow_fresh,
3138
+ team,
2969
3139
  ) {
2970
3140
  Ok(crate::lifecycle::StartAgentOutcome::Running {
2971
3141
  env,
@@ -2994,14 +3164,19 @@ pub mod lifecycle_port {
2994
3164
  }),
2995
3165
  )),
2996
3166
  Ok(crate::lifecycle::StartAgentOutcome::Noop { env, target }) => {
2997
- Ok(agent_action_value(agent, workspace, team, json!({
2998
- "ok": true,
2999
- "agent_id": env.agent_id.as_str(),
3000
- "status": "already_running",
3001
- "target": target,
3002
- "coordinator_started": env.coordinator_started,
3003
- "state_file": env.state_file.to_string_lossy(),
3004
- })))
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
+ ))
3005
3180
  }
3006
3181
  Ok(crate::lifecycle::StartAgentOutcome::Paused { agent_id }) => Ok(json!({
3007
3182
  "ok": false,
@@ -3034,13 +3209,7 @@ pub mod lifecycle_port {
3034
3209
  team: Option<&str>,
3035
3210
  ) -> Result<Value, CliError> {
3036
3211
  let agent_id = crate::model::ids::AgentId::new(agent);
3037
- match crate::lifecycle::reset_agent(
3038
- workspace,
3039
- &agent_id,
3040
- discard_session,
3041
- true,
3042
- team,
3043
- ) {
3212
+ match crate::lifecycle::reset_agent(workspace, &agent_id, discard_session, true, team) {
3044
3213
  Ok(crate::lifecycle::ResetAgentOutcome::Reset {
3045
3214
  env,
3046
3215
  start_mode,
@@ -3088,7 +3257,12 @@ pub mod lifecycle_port {
3088
3257
  let agent_id = crate::model::ids::AgentId::new(agent);
3089
3258
  let source = (!role_file.is_empty()).then(|| Path::new(role_file));
3090
3259
  match crate::lifecycle::role_config::add_agent_from_role(
3091
- workspace, &agent_id, source, role_config, force, team,
3260
+ workspace,
3261
+ &agent_id,
3262
+ source,
3263
+ role_config,
3264
+ force,
3265
+ team,
3092
3266
  ) {
3093
3267
  Ok(report) => Ok(agent_action_value(
3094
3268
  agent,
@@ -3107,7 +3281,12 @@ pub mod lifecycle_port {
3107
3281
  Err(e) => Ok(error_value(e)),
3108
3282
  }
3109
3283
  }
3110
- 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 {
3111
3290
  if value.get("ok").and_then(Value::as_bool) == Some(true) {
3112
3291
  if let Some(object) = value.as_object_mut() {
3113
3292
  if let Some(command) = super::adapters::send_command(agent, workspace, team) {
@@ -3748,10 +3927,7 @@ pub mod lifecycle_port {
3748
3927
  let mut default = detail.clone();
3749
3928
  compact_quick_start_value(&mut default);
3750
3929
  assert_eq!(default["status"], json!("leader_bind_refused"));
3751
- assert_eq!(
3752
- default["reason"],
3753
- json!("provider_unresolved")
3754
- );
3930
+ assert_eq!(default["reason"], json!("provider_unresolved"));
3755
3931
  assert_eq!(
3756
3932
  default["next_actions"],
3757
3933
  json!(["set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"])
@@ -3771,14 +3947,18 @@ pub mod lifecycle_port {
3771
3947
  );
3772
3948
  assert!(default["readiness"].get("all_spawned").is_none());
3773
3949
  assert!(default["readiness"].get("all_attached_receiver").is_none());
3774
- assert!(default["readiness"].get("leader_receiver_attached").is_none());
3950
+ assert!(default["readiness"]
3951
+ .get("leader_receiver_attached")
3952
+ .is_none());
3775
3953
  assert_eq!(
3776
3954
  default["worker_readiness"]["all_workers_spawned"],
3777
3955
  json!(true)
3778
3956
  );
3779
3957
  assert!(default["worker_readiness"].get("all_spawned").is_none());
3780
3958
  assert!(detail["readiness"].get("all_spawned").is_some());
3781
- assert!(detail["worker_readiness"].get("leader_receiver_attached").is_some());
3959
+ assert!(detail["worker_readiness"]
3960
+ .get("leader_receiver_attached")
3961
+ .is_some());
3782
3962
  }
3783
3963
 
3784
3964
  #[test]
@@ -3934,7 +4114,10 @@ pub mod lifecycle_port {
3934
4114
  compact_restart_value(&mut default);
3935
4115
  assert_eq!(default["status"], json!("partial"));
3936
4116
  assert_eq!(default["reason"], json!("restart_agent_failed"));
3937
- 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
+ );
3938
4121
  assert!(default.get("coordinator").is_none());
3939
4122
  assert!(detail.get("coordinator").is_some());
3940
4123
  }
@@ -3974,7 +4157,9 @@ pub mod lifecycle_port {
3974
4157
  &transport,
3975
4158
  )
3976
4159
  .expect("first quick-start");
3977
- let crate::lifecycle::QuickStartReport::Ready { team: ready_team, .. } = &first
4160
+ let crate::lifecycle::QuickStartReport::Ready {
4161
+ team: ready_team, ..
4162
+ } = &first
3978
4163
  else {
3979
4164
  panic!("first producer must return Ready; got {first:?}");
3980
4165
  };
@@ -3991,7 +4176,7 @@ pub mod lifecycle_port {
3991
4176
  "team-agent".to_string(),
3992
4177
  "send".to_string(),
3993
4178
  "implementer".to_string(),
3994
- "MESSAGE".to_string(),
4179
+ "请完成任务并把答案回复给 leader。".to_string(),
3995
4180
  "--workspace".to_string(),
3996
4181
  team_dir.to_string_lossy().into_owned(),
3997
4182
  "--team".to_string(),
@@ -4028,7 +4213,7 @@ pub mod lifecycle_port {
4028
4213
  "team-agent".to_string(),
4029
4214
  "send".to_string(),
4030
4215
  "implementer".to_string(),
4031
- "MESSAGE".to_string(),
4216
+ "请完成任务并把答案回复给 leader。".to_string(),
4032
4217
  "--workspace".to_string(),
4033
4218
  team_dir.to_string_lossy().into_owned(),
4034
4219
  "--team".to_string(),
@@ -4053,10 +4238,7 @@ pub mod lifecycle_port {
4053
4238
  value
4054
4239
  .get("agent_ids")
4055
4240
  .and_then(Value::as_array)
4056
- .map(|items| items
4057
- .iter()
4058
- .filter_map(Value::as_str)
4059
- .collect::<Vec<_>>()),
4241
+ .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
4060
4242
  Some(vec!["sol", "luna"])
4061
4243
  );
4062
4244
  }
@@ -4086,14 +4268,15 @@ pub mod lifecycle_port {
4086
4268
 
4087
4269
  #[test]
4088
4270
  fn existing_runtime_send_guidance_does_not_pair_alice_with_bob() {
4089
- let mut value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
4090
- team: Some("bob".to_string()),
4091
- session_name: Some(crate::transport::SessionName::new("team-bob")),
4092
- state_path: Some(PathBuf::from("/tmp/state.json")),
4093
- next_actions: vec!["restart".to_string()],
4094
- attach_commands: Vec::new(),
4095
- agent_ids: vec!["bob".to_string()],
4096
- });
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
+ });
4097
4280
  crate::cli::adapters::append_send_guidance(
4098
4281
  &mut value,
4099
4282
  Path::new("/tmp/ws"),
@@ -4188,7 +4371,7 @@ pub mod lifecycle_port {
4188
4371
  value["reason"] = json!(reason);
4189
4372
  }
4190
4373
  value
4191
- },
4374
+ }
4192
4375
  crate::lifecycle::RestartReport::Partial {
4193
4376
  session_name,
4194
4377
  agents,
@@ -4242,7 +4425,7 @@ pub mod lifecycle_port {
4242
4425
  value["leader_bind_reason"] = json!(reason);
4243
4426
  }
4244
4427
  value
4245
- },
4428
+ }
4246
4429
  crate::lifecycle::RestartReport::Failed {
4247
4430
  session_name,
4248
4431
  failed_agents,
@@ -5253,7 +5436,11 @@ pub mod diagnose_port {
5253
5436
  ///
5254
5437
  /// `orphan_gate(fix, confirm)`(`--gate orphans`)。CI gate。
5255
5438
  #[cfg(test)]
5256
- 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> {
5257
5444
  crate::diagnose::orphans::orphan_gate_json(workspace, fix, confirm)
5258
5445
  }
5259
5446
  ///