@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +33 -0
- package/README.zh.md +30 -0
- package/crates/team-agent/src/cli/adapters.rs +234 -73
- package/crates/team-agent/src/cli/emit.rs +494 -201
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -161
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +212 -0
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +56 -106
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +22 -13
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/start.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +1457 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/provider/argv_route.rs +409 -0
- package/crates/team-agent/src/provider/mod.rs +3 -2
- package/package.json +4 -4
- 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 =
|
|
52
|
-
|
|
53
|
-
pub(crate) const
|
|
54
|
-
pub(crate) const
|
|
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
|
|
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(||
|
|
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|
|
|
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) =
|
|
762
|
-
.filter(|generation| !generation.is_empty())
|
|
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
|
|
770
|
-
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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(
|
|
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
|
-
(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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 =
|
|
828
|
-
|
|
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!(
|
|
903
|
+
assert_eq!(
|
|
904
|
+
session_ownership(&workspace, &state, &transport, &session, &targets),
|
|
905
|
+
expected
|
|
906
|
+
);
|
|
834
907
|
if expected == SessionOwnership::Foreign {
|
|
835
|
-
assert!(
|
|
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-{}-{}",
|
|
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),
|
|
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>(
|
|
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 {
|
|
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
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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!(
|
|
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
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
1020
|
+
&& captured
|
|
1021
|
+
.text
|
|
1022
|
+
.contains("Provider exited; this pane no longer accepts input.")
|
|
896
1023
|
{
|
|
897
1024
|
break;
|
|
898
1025
|
}
|
|
899
|
-
assert!(
|
|
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!(
|
|
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 =
|
|
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!(
|
|
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(
|
|
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 {
|
|
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!(
|
|
958
|
-
|
|
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 =
|
|
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
|
|
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(||
|
|
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 _ =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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 [
|
|
2554
|
-
|
|
2555
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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(
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
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,
|
|
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(
|
|
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"]
|
|
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"]
|
|
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!(
|
|
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 {
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 =
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
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(
|
|
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
|
///
|