@team-agent/installer 0.3.15 → 0.3.16

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.
@@ -858,6 +858,7 @@ fn save_launched_team_state_for_key(
858
858
  );
859
859
  }
860
860
  promote_launched_binding_from_team_entry(&mut launched, &launched_key);
861
+ preserve_existing_leader_topology(&existing, &launched_key, &mut launched);
861
862
  drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
862
863
  drop_bare_worker_seeded_owner(&mut launched, &launched_key);
863
864
  let merged = if team_key.is_some() {
@@ -871,6 +872,28 @@ fn save_launched_team_state_for_key(
871
872
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))
872
873
  }
873
874
 
875
+ fn preserve_existing_leader_topology(
876
+ existing: &serde_json::Value,
877
+ launched_key: &str,
878
+ launched: &mut serde_json::Value,
879
+ ) {
880
+ let Some(obj) = launched.as_object_mut() else {
881
+ return;
882
+ };
883
+ let existing_team = existing
884
+ .get("teams")
885
+ .and_then(serde_json::Value::as_object)
886
+ .and_then(|teams| teams.get(launched_key))
887
+ .unwrap_or(existing);
888
+ for key in ["is_external_leader", "leader_client"] {
889
+ if !obj.contains_key(key) {
890
+ if let Some(value) = existing_team.get(key).or_else(|| existing.get(key)) {
891
+ obj.insert(key.to_string(), value.clone());
892
+ }
893
+ }
894
+ }
895
+ }
896
+
874
897
  fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
875
898
  if has_positive_caller_leader_env() {
876
899
  return;
@@ -2834,7 +2857,12 @@ pub fn quick_start_with_transport_in_workspace_with_display(
2834
2857
  // asynchronously after spawn), so the verdict is PendingToolLoad — never
2835
2858
  // bare Ready.
2836
2859
  let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
2837
- let attach_windows = started_attach_window_names(&launch.started);
2860
+ let attach_windows = load_runtime_state(&workspace)
2861
+ .ok()
2862
+ .map(|state| {
2863
+ attach_window_names_with_managed_leader(&state, started_attach_window_names(&launch.started))
2864
+ })
2865
+ .unwrap_or_else(|| started_attach_window_names(&launch.started));
2838
2866
  let attach_commands = attach_commands_for_runtime_windows(
2839
2867
  launch.tmux_endpoint.as_deref(),
2840
2868
  &workspace,
@@ -2934,8 +2962,33 @@ fn started_attach_window_names(started: &[StartedAgent]) -> Vec<String> {
2934
2962
  windows
2935
2963
  }
2936
2964
 
2965
+ pub(crate) fn attach_window_names_for_state_agents<'a>(
2966
+ state: &serde_json::Value,
2967
+ agent_ids: impl IntoIterator<Item = &'a str>,
2968
+ ) -> Vec<String> {
2969
+ let windows = agent_ids
2970
+ .into_iter()
2971
+ .map(|agent_id| {
2972
+ state
2973
+ .get("agents")
2974
+ .and_then(serde_json::Value::as_object)
2975
+ .and_then(|agents| agents.get(agent_id))
2976
+ .and_then(|agent| {
2977
+ agent
2978
+ .get("layout_window")
2979
+ .or_else(|| agent.get("window"))
2980
+ .and_then(serde_json::Value::as_str)
2981
+ .filter(|window| !window.is_empty())
2982
+ })
2983
+ .unwrap_or(agent_id)
2984
+ .to_string()
2985
+ })
2986
+ .collect::<Vec<_>>();
2987
+ attach_window_names_with_managed_leader(state, windows)
2988
+ }
2989
+
2937
2990
  fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
2938
- let mut windows = state
2991
+ let windows = state
2939
2992
  .get("agents")
2940
2993
  .and_then(serde_json::Value::as_object)
2941
2994
  .map(|agents| {
@@ -2952,11 +3005,28 @@ fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
2952
3005
  .collect::<Vec<_>>()
2953
3006
  })
2954
3007
  .unwrap_or_default();
3008
+ attach_window_names_with_managed_leader(state, windows)
3009
+ }
3010
+
3011
+ fn attach_window_names_with_managed_leader(
3012
+ state: &serde_json::Value,
3013
+ mut windows: Vec<String>,
3014
+ ) -> Vec<String> {
3015
+ if state_uses_managed_leader(state) {
3016
+ windows.push("leader".to_string());
3017
+ }
2955
3018
  windows.sort();
2956
3019
  windows.dedup();
2957
3020
  windows
2958
3021
  }
2959
3022
 
3023
+ fn state_uses_managed_leader(state: &serde_json::Value) -> bool {
3024
+ state
3025
+ .get("is_external_leader")
3026
+ .and_then(serde_json::Value::as_bool)
3027
+ .is_some_and(|external| !external)
3028
+ }
3029
+
2960
3030
  /// BUG-7 helper: derive a [`QuickStartReadiness`] verdict from the just-written
2961
3031
  /// runtime state. Reads `agents[*].status`; any non-`running` agent flips the
2962
3032
  /// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
@@ -395,13 +395,17 @@ pub fn restart_with_transport_with_session_convergence_deadline(
395
395
  restart_readiness_deadline(readiness_deadline_ms),
396
396
  restart_readiness_poll_interval(),
397
397
  )?;
398
- let attach_commands = crate::tmux_backend::attach_commands_for_windows(
399
- &selected.run_workspace,
400
- &session_name,
398
+ let attach_windows = crate::lifecycle::launch::attach_window_names_for_state_agents(
399
+ &state,
401
400
  successful_agents
402
401
  .iter()
403
402
  .map(|decision| decision.agent_id.as_str()),
404
403
  );
404
+ let attach_commands = crate::tmux_backend::attach_commands_for_windows(
405
+ &selected.run_workspace,
406
+ &session_name,
407
+ attach_windows.iter().map(String::as_str),
408
+ );
405
409
  let mut next_actions = attach_commands.clone();
406
410
  if !failed_agents.is_empty() {
407
411
  next_actions.extend(restart_failure_next_actions(&failed_agents));
@@ -1205,6 +1205,69 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
1205
1205
  assert_eq!(state["tmux_socket"], json!(endpoint));
1206
1206
  }
1207
1207
 
1208
+ #[test]
1209
+ fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command() {
1210
+ let team = quick_start_team_dir(QS_VALID_ROLE);
1211
+ let workspace = team.parent().expect("team_workspace(team_dir) = parent");
1212
+ seed_healthy_coordinator(workspace);
1213
+ crate::state::persist::save_runtime_state(
1214
+ workspace,
1215
+ &json!({
1216
+ "active_team_key": "teamdir",
1217
+ "session_name": "team-teamdir",
1218
+ "is_external_leader": false,
1219
+ "leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
1220
+ "leader_receiver": {"mode": "direct_tmux", "status": "attached", "pane_id": "%42"},
1221
+ "team_owner": {"pane_id": "%42", "owner_epoch": 1},
1222
+ "agents": {},
1223
+ "teams": {
1224
+ "teamdir": {
1225
+ "session_name": "team-teamdir",
1226
+ "is_external_leader": false,
1227
+ "leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
1228
+ "agents": {}
1229
+ }
1230
+ }
1231
+ }),
1232
+ )
1233
+ .expect("seed managed leader state");
1234
+ let transport = OfflineTransport::new();
1235
+
1236
+ let report = quick_start_with_transport(&team, None, true, true, None, &transport)
1237
+ .expect("quick_start_with_transport must reach Ready");
1238
+ let attach_commands = match report {
1239
+ QuickStartReport::Ready { attach_commands, .. } => attach_commands,
1240
+ other => panic!("quick_start must reach Ready; got {other:?}"),
1241
+ };
1242
+
1243
+ assert!(
1244
+ attach_commands.iter().any(|cmd| cmd.contains(":leader")),
1245
+ "managed topology quick-start output must include the leader window attach command: {attach_commands:?}"
1246
+ );
1247
+ let (_raw, state) = raw_runtime_state(workspace);
1248
+ assert_eq!(state["is_external_leader"], json!(false));
1249
+ assert_eq!(state["leader_client"]["diagnostic_only"], json!(true));
1250
+ }
1251
+
1252
+ #[test]
1253
+ fn attach_window_names_for_state_agents_include_managed_leader_and_layout_windows() {
1254
+ let state = json!({
1255
+ "is_external_leader": false,
1256
+ "agents": {
1257
+ "w1": {"layout_window": "team-w1", "window": "w1"},
1258
+ "w2": {"layout_window": "team-w1", "window": "w2"},
1259
+ "w4": {"layout_window": "team-w2", "window": "w4"}
1260
+ }
1261
+ });
1262
+
1263
+ let windows = crate::lifecycle::launch::attach_window_names_for_state_agents(
1264
+ &state,
1265
+ ["w1", "w2", "w4"].into_iter(),
1266
+ );
1267
+
1268
+ assert_eq!(windows, vec!["leader", "team-w1", "team-w2"]);
1269
+ }
1270
+
1208
1271
  #[test]
1209
1272
  fn quick_start_no_display_keeps_one_window_per_agent() {
1210
1273
  let roles = ["w1", "w2"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.3.15",
24
- "@team-agent/cli-darwin-x64": "0.3.15",
25
- "@team-agent/cli-linux-x64": "0.3.15"
23
+ "@team-agent/cli-darwin-arm64": "0.3.16",
24
+ "@team-agent/cli-darwin-x64": "0.3.16",
25
+ "@team-agent/cli-linux-x64": "0.3.16"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",