@team-agent/installer 0.3.12 → 0.3.14

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 (35) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +23 -0
  4. package/crates/team-agent/src/cli/leader.rs +2 -5
  5. package/crates/team-agent/src/cli/mod.rs +258 -79
  6. package/crates/team-agent/src/cli/tests/compile.rs +69 -0
  7. package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
  9. package/crates/team-agent/src/compiler.rs +30 -0
  10. package/crates/team-agent/src/coordinator/tick.rs +16 -83
  11. package/crates/team-agent/src/leader/lease.rs +4 -20
  12. package/crates/team-agent/src/leader/mod.rs +3 -1
  13. package/crates/team-agent/src/leader/owner_bind.rs +46 -48
  14. package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
  15. package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
  16. package/crates/team-agent/src/leader/rediscover.rs +34 -17
  17. package/crates/team-agent/src/leader/start.rs +279 -4
  18. package/crates/team-agent/src/lifecycle/launch.rs +279 -24
  19. package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
  20. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +343 -49
  21. package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
  22. package/crates/team-agent/src/lifecycle/tests/core.rs +181 -45
  23. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +390 -61
  24. package/crates/team-agent/src/lifecycle/types.rs +28 -0
  25. package/crates/team-agent/src/messaging/delivery.rs +5 -1
  26. package/crates/team-agent/src/messaging/helpers.rs +1 -1
  27. package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
  28. package/crates/team-agent/src/provider/adapter.rs +6 -3
  29. package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
  30. package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
  31. package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
  32. package/crates/team-agent/src/tmux_backend.rs +5 -0
  33. package/crates/team-agent/src/transport/test_support.rs +22 -7
  34. package/crates/team-agent/src/transport.rs +4 -0
  35. package/package.json +4 -4
@@ -9,7 +9,7 @@ use crate::model::ids::AgentId;
9
9
  use crate::model::permissions::{self, AgentPermissionInput};
10
10
  use crate::model::yaml::{self, Value};
11
11
  use crate::state::persist::{load_runtime_state, save_runtime_state};
12
- use crate::transport::{SessionName, Target, Transport, WindowName};
12
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
13
13
 
14
14
  use super::*;
15
15
 
@@ -595,7 +595,7 @@ fn merge_workspace_team_state_with_key(
595
595
  launched_key: &str,
596
596
  ) -> serde_json::Value {
597
597
  let mut launched_obj = launched.as_object().cloned().unwrap_or_default();
598
- let mut teams = launched
598
+ let mut teams = existing
599
599
  .get("teams")
600
600
  .and_then(serde_json::Value::as_object)
601
601
  .cloned()
@@ -638,6 +638,41 @@ fn merge_workspace_team_state_with_key(
638
638
  serde_json::Value::Object(merged)
639
639
  }
640
640
 
641
+ #[cfg(test)]
642
+ mod merge_workspace_team_state_with_key_tests {
643
+ use super::*;
644
+ use serde_json::json;
645
+
646
+ #[test]
647
+ fn empty_top_level_existing_session_preserves_existing_teams() {
648
+ let existing = json!({
649
+ "session_name": "",
650
+ "active_team_key": "parent",
651
+ "teams": {
652
+ "parent": {
653
+ "session_name": "team-parent",
654
+ "agents": {"parent_worker": {"status": "running"}}
655
+ }
656
+ }
657
+ });
658
+ let launched = json!({
659
+ "session_name": "team-child",
660
+ "agents": {"child_worker": {"status": "running"}}
661
+ });
662
+ let merged = merge_workspace_team_state_with_key(&existing, &launched, "child");
663
+ assert_eq!(
664
+ merged.pointer("/teams/parent/session_name"),
665
+ Some(&json!("team-parent")),
666
+ "existing.teams must survive even when existing.session_name is empty: {merged}"
667
+ );
668
+ assert_eq!(
669
+ merged.pointer("/teams/child/session_name"),
670
+ Some(&json!("team-child")),
671
+ "launched team must still be inserted under its runtime key: {merged}"
672
+ );
673
+ }
674
+ }
675
+
641
676
  fn promote_launched_binding_from_team_entry(launched: &mut serde_json::Value, launched_key: &str) {
642
677
  let entry = launched
643
678
  .get("teams")
@@ -849,6 +884,29 @@ fn write_invalid_leader_pane_env_warning(workspaces: &[&Path], pane_id_raw: &str
849
884
  }
850
885
  }
851
886
 
887
+ fn warn_ignored_owner_team_id(workspace: &Path, team_dir: &Path, runtime_team_key: &str) {
888
+ let Ok(Some(ignored)) = crate::compiler::ignored_owner_team_id_from_team_md(team_dir) else {
889
+ return;
890
+ };
891
+ eprintln!("Warning: ignored TEAM.md {}={}", ignored.field, ignored.value);
892
+ eprintln!("Reason: owner identity is the canonical runtime team key ({runtime_team_key}), not TEAM.md front matter");
893
+ eprintln!("Action: remove {} from TEAM.md", ignored.field);
894
+ if let Err(err) = crate::event_log::EventLog::new(workspace).write(
895
+ "spec.field_ignored",
896
+ serde_json::json!({
897
+ "field": ignored.field,
898
+ "source": team_dir.join("TEAM.md").to_string_lossy().to_string(),
899
+ "value": ignored.value,
900
+ "warning": "ignored user-set owner_team_id",
901
+ "reason": "owner identity is derived from the canonical runtime team key",
902
+ "action": "remove owner_team_id from TEAM.md",
903
+ "runtime_team_key": runtime_team_key,
904
+ }),
905
+ ) {
906
+ eprintln!("Warning: spec.field_ignored event write failed: {err}");
907
+ }
908
+ }
909
+
852
910
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
853
911
  pub(crate) enum LeaderPaneEnvState {
854
912
  Live,
@@ -932,12 +990,13 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
932
990
  let Some(owner) = unbound_launched_owner(launched, launched_key) else {
933
991
  return;
934
992
  };
935
- let provider = launched
936
- .get("team_owner")
937
- .and_then(|owner| owner.get("provider"))
993
+ let Some(provider) = owner
994
+ .get("provider")
938
995
  .and_then(serde_json::Value::as_str)
939
996
  .filter(|provider| !provider.is_empty())
940
- .unwrap_or("codex");
997
+ else {
998
+ return;
999
+ };
941
1000
  let owner_epoch = 1u64;
942
1001
  let receiver = serde_json::json!({
943
1002
  "mode": "direct_tmux",
@@ -958,12 +1017,7 @@ fn unbound_launched_owner(
958
1017
  launched: &serde_json::Value,
959
1018
  launched_key: &str,
960
1019
  ) -> Option<serde_json::Value> {
961
- let provider = launched
962
- .get("team_owner")
963
- .and_then(|owner| owner.get("provider"))
964
- .and_then(serde_json::Value::as_str)
965
- .filter(|provider| !provider.is_empty())
966
- .unwrap_or("codex");
1020
+ let provider = unbound_launched_provider(launched)?;
967
1021
  let machine_fingerprint = launched
968
1022
  .get("team_owner")
969
1023
  .and_then(|owner| owner.get("machine_fingerprint"))
@@ -994,6 +1048,193 @@ fn unbound_launched_owner(
994
1048
  }))
995
1049
  }
996
1050
 
1051
+ fn unbound_launched_provider(launched: &serde_json::Value) -> Option<String> {
1052
+ if let Some(provider) = launched
1053
+ .get("team_owner")
1054
+ .and_then(|owner| owner.get("provider"))
1055
+ .and_then(serde_json::Value::as_str)
1056
+ .filter(|provider| !provider.is_empty())
1057
+ .and_then(parse_provider)
1058
+ .and_then(provider_wire_string)
1059
+ {
1060
+ return Some(provider);
1061
+ }
1062
+ let pane = launched
1063
+ .get("team_owner")
1064
+ .and_then(|owner| owner.get("pane_id"))
1065
+ .and_then(serde_json::Value::as_str)
1066
+ .filter(|pane| !pane.is_empty())?;
1067
+ let target = PaneId::new(pane);
1068
+ attributed_provider_for_pane_across_tmux_sockets(&target).and_then(provider_wire_string)
1069
+ }
1070
+
1071
+ fn provider_wire_string(provider: Provider) -> Option<String> {
1072
+ serde_json::to_value(provider)
1073
+ .ok()
1074
+ .and_then(|value| value.as_str().map(str::to_string))
1075
+ }
1076
+
1077
+ fn attributed_provider_for_pane_across_tmux_sockets(pane: &PaneId) -> Option<Provider> {
1078
+ crate::tmux_backend::tmux_socket_endpoints()
1079
+ .into_iter()
1080
+ .filter_map(|endpoint| {
1081
+ crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
1082
+ .list_targets()
1083
+ .ok()
1084
+ })
1085
+ .flatten()
1086
+ .find(|info| info.pane_id == *pane)
1087
+ .and_then(|info| crate::leader::attribute_pane_provider(&info))
1088
+ }
1089
+
1090
+ fn caller_provider_for_seed_with_lookup(
1091
+ caller: &crate::state::owner_gate::CallerIdentity,
1092
+ lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
1093
+ ) -> Option<String> {
1094
+ if !caller.provider.is_empty() {
1095
+ if let Some(provider) = parse_provider(&caller.provider).and_then(provider_wire_string) {
1096
+ return Some(provider);
1097
+ }
1098
+ }
1099
+ (!caller.pane_id.is_empty())
1100
+ .then(|| PaneId::new(&caller.pane_id))
1101
+ .and_then(|pane| lookup_pane_provider(&pane))
1102
+ .and_then(provider_wire_string)
1103
+ }
1104
+
1105
+ #[cfg(test)]
1106
+ mod e22_unbound_owner_provider_tests {
1107
+ use super::*;
1108
+ use crate::state::owner_gate::CallerIdentity;
1109
+
1110
+ #[test]
1111
+ fn unbound_owner_preserves_explicit_copilot_provider() {
1112
+ let mut launched = serde_json::json!({
1113
+ "workspace": "/tmp/team-agent-e22",
1114
+ "team_owner": {
1115
+ "provider": "copilot",
1116
+ "machine_fingerprint": "machine"
1117
+ }
1118
+ });
1119
+
1120
+ seed_unbound_launched_owner(&mut launched, "team-e22");
1121
+
1122
+ assert_eq!(
1123
+ launched
1124
+ .pointer("/team_owner/provider")
1125
+ .and_then(serde_json::Value::as_str),
1126
+ Some("copilot")
1127
+ );
1128
+ assert_eq!(
1129
+ launched
1130
+ .pointer("/leader_receiver/provider")
1131
+ .and_then(serde_json::Value::as_str),
1132
+ Some("copilot")
1133
+ );
1134
+ }
1135
+
1136
+ #[test]
1137
+ fn unbound_owner_without_attributed_provider_does_not_default_codex() {
1138
+ let mut launched = serde_json::json!({
1139
+ "workspace": "/tmp/team-agent-e22",
1140
+ "team_owner": {
1141
+ "machine_fingerprint": "machine"
1142
+ }
1143
+ });
1144
+
1145
+ seed_unbound_launched_owner(&mut launched, "team-e22");
1146
+
1147
+ assert!(
1148
+ launched.get("leader_receiver").is_none(),
1149
+ "unattributed unbound owner must not seed a codex receiver: {launched}"
1150
+ );
1151
+ assert!(
1152
+ launched
1153
+ .pointer("/team_owner/provider")
1154
+ .and_then(serde_json::Value::as_str)
1155
+ != Some("codex"),
1156
+ "unattributed unbound owner must not silently become codex: {launched}"
1157
+ );
1158
+ }
1159
+
1160
+ fn caller(provider: &str, pane_id: &str) -> CallerIdentity {
1161
+ CallerIdentity {
1162
+ pane_id: pane_id.to_string(),
1163
+ provider: provider.to_string(),
1164
+ machine_fingerprint: "machine".to_string(),
1165
+ leader_session_uuid: "leader-uuid".to_string(),
1166
+ leader_session_uuid_source: "derived".to_string(),
1167
+ }
1168
+ }
1169
+
1170
+ #[test]
1171
+ fn env_seed_attributes_in_tmux_node_form_copilot_from_caller_pane() {
1172
+ let mut state = serde_json::json!({
1173
+ "workspace": "/tmp/team-agent-e22",
1174
+ "leader": {"provider": "copilot"},
1175
+ });
1176
+
1177
+ assert!(seed_launched_owner_from_caller_with_provider_lookup(
1178
+ &mut state,
1179
+ caller("", "%0"),
1180
+ |pane| (pane.as_str() == "%0").then_some(Provider::Copilot),
1181
+ ));
1182
+
1183
+ assert_eq!(
1184
+ state
1185
+ .pointer("/team_owner/provider")
1186
+ .and_then(serde_json::Value::as_str),
1187
+ Some("copilot")
1188
+ );
1189
+ assert_eq!(
1190
+ state
1191
+ .pointer("/leader_receiver/provider")
1192
+ .and_then(serde_json::Value::as_str),
1193
+ Some("copilot")
1194
+ );
1195
+ }
1196
+
1197
+ #[test]
1198
+ fn env_seed_unknown_caller_pane_does_not_default_codex() {
1199
+ let mut state = serde_json::json!({
1200
+ "workspace": "/tmp/team-agent-e22",
1201
+ "leader": {"provider": "copilot"},
1202
+ });
1203
+
1204
+ assert!(seed_launched_owner_from_caller_with_provider_lookup(
1205
+ &mut state,
1206
+ caller("", "%0"),
1207
+ |_| None,
1208
+ ));
1209
+ assert_eq!(
1210
+ state
1211
+ .pointer("/team_owner/pane_id")
1212
+ .and_then(serde_json::Value::as_str),
1213
+ Some("%0")
1214
+ );
1215
+ assert_eq!(
1216
+ state
1217
+ .pointer("/leader_receiver/pane_id")
1218
+ .and_then(serde_json::Value::as_str),
1219
+ Some("%0")
1220
+ );
1221
+ assert!(
1222
+ state
1223
+ .pointer("/leader_receiver/provider")
1224
+ .and_then(serde_json::Value::as_str)
1225
+ != Some("codex"),
1226
+ "unknown caller pane must not silently seed a codex receiver: {state}"
1227
+ );
1228
+ assert!(
1229
+ state
1230
+ .pointer("/team_owner/provider")
1231
+ .and_then(serde_json::Value::as_str)
1232
+ != Some("codex"),
1233
+ "unknown caller pane must not silently become codex: {state}"
1234
+ );
1235
+ }
1236
+ }
1237
+
997
1238
  fn owner_pane_belongs_to_other_team(
998
1239
  existing: &serde_json::Value,
999
1240
  launched_key: &str,
@@ -2144,6 +2385,7 @@ pub fn quick_start_with_transport_in_workspace(
2144
2385
  let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
2145
2386
  runtime_team_key_for_spec(&agents_dir.join("team.spec.yaml"), &spec, &session_name)
2146
2387
  });
2388
+ warn_ignored_owner_team_id(workspace.as_path(), agents_dir, &state_team_key);
2147
2389
  // E5 spec 迁移:spec 写到 .team/runtime/<team_key>/(中间产物,绝不落用户目录 agents_dir)。
2148
2390
  // Bug2:原子写(tmp+rename),避免半截 spec。
2149
2391
  let spec_path = crate::model::paths::runtime_spec_path(&workspace, &state_team_key);
@@ -3565,19 +3807,26 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
3565
3807
  ) else {
3566
3808
  return false;
3567
3809
  };
3568
- let provider = if caller.provider.is_empty() {
3569
- "codex".to_string()
3570
- } else {
3571
- caller.provider
3572
- };
3573
- let pane_id = caller.pane_id;
3574
- if pane_id.is_empty() {
3810
+ seed_launched_owner_from_caller_with_provider_lookup(
3811
+ state,
3812
+ caller,
3813
+ attributed_provider_for_pane_across_tmux_sockets,
3814
+ )
3815
+ }
3816
+
3817
+ fn seed_launched_owner_from_caller_with_provider_lookup(
3818
+ state: &mut serde_json::Value,
3819
+ caller: crate::state::owner_gate::CallerIdentity,
3820
+ lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
3821
+ ) -> bool {
3822
+ if caller.pane_id.is_empty() {
3575
3823
  return false;
3576
3824
  }
3825
+ let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
3826
+ let pane_id = caller.pane_id;
3577
3827
  let owner_epoch = 1u64;
3578
- let owner = serde_json::json!({
3828
+ let mut owner = serde_json::json!({
3579
3829
  "pane_id": pane_id,
3580
- "provider": provider.clone(),
3581
3830
  "machine_fingerprint": caller.machine_fingerprint,
3582
3831
  "leader_session_uuid": caller.leader_session_uuid,
3583
3832
  "owner_epoch": owner_epoch,
@@ -3587,17 +3836,23 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
3587
3836
  .or_else(|_| std::env::var("USERNAME"))
3588
3837
  .unwrap_or_default(),
3589
3838
  });
3590
- let receiver = serde_json::json!({
3839
+ let mut receiver = serde_json::json!({
3591
3840
  "mode": "direct_tmux",
3592
3841
  "status": "attached",
3593
- "provider": provider,
3594
3842
  "pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
3595
3843
  "pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
3596
3844
  "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
3597
3845
  "owner_epoch": owner_epoch,
3598
3846
  "discovery": "quick_start",
3599
3847
  });
3600
- let mut receiver = receiver;
3848
+ if let Some(provider) = provider.as_ref() {
3849
+ if let Some(owner) = owner.as_object_mut() {
3850
+ owner.insert("provider".to_string(), serde_json::json!(provider));
3851
+ }
3852
+ if let Some(receiver) = receiver.as_object_mut() {
3853
+ receiver.insert("provider".to_string(), serde_json::json!(provider));
3854
+ }
3855
+ }
3601
3856
  if let (Some(receiver), Some(socket)) = (
3602
3857
  receiver.as_object_mut(),
3603
3858
  crate::tmux_backend::socket_name_from_tmux_env(),
@@ -36,12 +36,11 @@ pub(super) fn spawn_agent_window(
36
36
  detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
37
37
  &detected_safety
38
38
  };
39
- let command_agent =
40
- crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
41
- agent,
42
- Some(agent_id.as_str()),
43
- provider,
44
- );
39
+ let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
40
+ agent,
41
+ Some(agent_id.as_str()),
42
+ provider,
43
+ );
45
44
  let system_prompt =
46
45
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
47
46
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -60,7 +59,8 @@ pub(super) fn spawn_agent_window(
60
59
  .and_then(|v| v.as_str())
61
60
  .map(str::to_string)
62
61
  .or_else(|| {
63
- let key = crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
62
+ let key =
63
+ crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
64
64
  (!key.is_empty()).then_some(key)
65
65
  });
66
66
  let mcp_config = adapter
@@ -72,8 +72,11 @@ pub(super) fn spawn_agent_window(
72
72
  agent_id.as_str(),
73
73
  team_id.as_deref().unwrap_or(""),
74
74
  );
75
- let mcp_config_path =
76
- crate::lifecycle::launch::write_worker_mcp_config(workspace, agent_id.as_str(), &mcp_config)?;
75
+ let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
76
+ workspace,
77
+ agent_id.as_str(),
78
+ &mcp_config,
79
+ )?;
77
80
  let profile_launch =
78
81
  crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
79
82
  workspace,
@@ -81,11 +84,7 @@ pub(super) fn spawn_agent_window(
81
84
  agent,
82
85
  Some(&mcp_config),
83
86
  )?;
84
- let command_model = profile_launch
85
- .command_overrides
86
- .model
87
- .as_deref()
88
- .or(model);
87
+ let command_model = profile_launch.command_overrides.model.as_deref().or(model);
89
88
  let context = crate::provider::ProviderCommandContext {
90
89
  auth_mode,
91
90
  mcp_config: Some(&mcp_config),
@@ -252,19 +251,30 @@ pub(super) fn resume_backing_exists_for_agent(
252
251
  rollout_path: Option<&RolloutPath>,
253
252
  ) -> bool {
254
253
  match provider {
254
+ provider if !provider_supports_resume(provider) => {
255
+ let _ = (workspace, agent_id, agent, session_id, rollout_path);
256
+ false
257
+ }
255
258
  Provider::Codex => rollout_path_exists(rollout_path),
256
259
  Provider::Claude | Provider::ClaudeCode => {
257
260
  rollout_path_exists(rollout_path)
258
261
  || event_log_transcript_exists(workspace, agent_id.as_str(), session_id.as_str())
259
262
  }
260
263
  Provider::Copilot => copilot_session_store_has_session(session_id.as_str()),
261
- Provider::GeminiCli | Provider::Fake => {
262
- let _ = agent;
263
- true
264
- }
264
+ Provider::GeminiCli | Provider::Fake => false,
265
265
  }
266
266
  }
267
267
 
268
+ pub(super) fn provider_supports_resume(provider: Provider) -> bool {
269
+ crate::provider::get_adapter(provider).caps().resume
270
+ }
271
+
272
+ pub(super) fn provider_wire_supports_resume(provider: &str) -> bool {
273
+ parse_provider(provider)
274
+ .map(provider_supports_resume)
275
+ .unwrap_or(false)
276
+ }
277
+
268
278
  fn rollout_path_exists(rollout_path: Option<&RolloutPath>) -> bool {
269
279
  rollout_path
270
280
  .as_ref()
@@ -308,13 +318,12 @@ fn copilot_session_store_has_session(session_id: &str) -> bool {
308
318
  ) else {
309
319
  return false;
310
320
  };
311
- conn
312
- .query_row(
313
- "select 1 from sessions where id = ?1 limit 1",
314
- [session_id],
315
- |_| Ok(()),
316
- )
317
- .is_ok()
321
+ conn.query_row(
322
+ "select 1 from sessions where id = ?1 limit 1",
323
+ [session_id],
324
+ |_| Ok(()),
325
+ )
326
+ .is_ok()
318
327
  }
319
328
 
320
329
  pub(crate) fn refresh_missing_provider_sessions(
@@ -469,7 +478,10 @@ pub(super) fn load_team_spec(workspace: &Path) -> Result<YamlValue, LifecycleErr
469
478
  yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))
470
479
  }
471
480
 
472
- pub(super) fn find_spec_agent<'a>(spec: &'a YamlValue, agent_id: &AgentId) -> Option<&'a YamlValue> {
481
+ pub(super) fn find_spec_agent<'a>(
482
+ spec: &'a YamlValue,
483
+ agent_id: &AgentId,
484
+ ) -> Option<&'a YamlValue> {
473
485
  let leader_is_agent = spec
474
486
  .get("leader")
475
487
  .and_then(|v| v.get("id"))
@@ -479,23 +491,23 @@ pub(super) fn find_spec_agent<'a>(spec: &'a YamlValue, agent_id: &AgentId) -> Op
479
491
  if leader_is_agent {
480
492
  return None;
481
493
  }
482
- spec.get("agents")?
483
- .as_list()?
484
- .iter()
485
- .find(|agent| {
486
- agent
487
- .get("id")
488
- .and_then(YamlValue::as_str)
489
- .map(|id| id == agent_id.as_str())
490
- .unwrap_or(false)
491
- })
494
+ spec.get("agents")?.as_list()?.iter().find(|agent| {
495
+ agent
496
+ .get("id")
497
+ .and_then(YamlValue::as_str)
498
+ .map(|id| id == agent_id.as_str())
499
+ .unwrap_or(false)
500
+ })
492
501
  }
493
502
 
494
503
  pub(super) fn unknown_worker(agent_id: &AgentId) -> LifecycleError {
495
504
  LifecycleError::RequirementUnmet(format!("unknown worker agent id: {agent_id}"))
496
505
  }
497
506
 
498
- pub(super) fn state_session_name_from_spec(state: &serde_json::Value, spec: &YamlValue) -> SessionName {
507
+ pub(super) fn state_session_name_from_spec(
508
+ state: &serde_json::Value,
509
+ spec: &YamlValue,
510
+ ) -> SessionName {
499
511
  state
500
512
  .get("session_name")
501
513
  .and_then(|v| v.as_str())
@@ -709,9 +721,7 @@ pub(super) fn agent_is_running(
709
721
  agent_id: &AgentId,
710
722
  transport: &dyn crate::transport::Transport,
711
723
  ) -> bool {
712
- let agent_state = state
713
- .get("agents")
714
- .and_then(|v| v.get(agent_id.as_str()));
724
+ let agent_state = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
715
725
  let status = agent_state
716
726
  .and_then(|v| v.get("status"))
717
727
  .and_then(|v| v.as_str())