@team-agent/installer 0.3.34 → 0.3.35

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 CHANGED
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.3.34"
569
+ version = "0.3.35"
570
570
  dependencies = [
571
571
  "anyhow",
572
572
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.34"
12
+ version = "0.3.35"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -158,13 +158,7 @@ pub mod lifecycle_port {
158
158
  crate::leader::LeaderLaunchStatus::Detached => true,
159
159
  crate::leader::LeaderLaunchStatus::NotStarted => false,
160
160
  };
161
- let leader_attach_command = if plan.is_external_leader {
162
- None
163
- } else {
164
- plan.session_name.as_ref().and_then(|session| {
165
- crate::tmux_backend::attach_command_for_workspace(cwd, session, "leader")
166
- })
167
- };
161
+ let leader_attach_command = leader_attach_command_for_plan(cwd, &plan);
168
162
  Ok(json!({
169
163
  "ok": ok,
170
164
  "provider": provider,
@@ -182,6 +176,18 @@ pub mod lifecycle_port {
182
176
  "session_name": plan.session_name.as_ref().map(|session| session.as_str().to_string()),
183
177
  }))
184
178
  }
179
+
180
+ pub(crate) fn leader_attach_command_for_plan(
181
+ cwd: &Path,
182
+ plan: &crate::leader::LeaderStartPlan,
183
+ ) -> Option<String> {
184
+ if plan.is_external_leader {
185
+ return None;
186
+ }
187
+ let session = plan.session_name.as_ref()?;
188
+ let window = plan.leader_window.as_ref()?;
189
+ crate::tmux_backend::attach_command_for_workspace(cwd, session, window.as_str())
190
+ }
185
191
  /// `runtime.shutdown`(`cmd_shutdown`)。
186
192
  pub fn shutdown(workspace: &Path, keep_logs: bool, team: Option<&str>) -> Result<Value, CliError> {
187
193
  let run_ws = crate::model::paths::canonical_run_workspace(workspace)
@@ -53,11 +53,15 @@ use rusqlite::params;
53
53
  let leader_attach_command = if is_external_leader {
54
54
  None
55
55
  } else {
56
+ let window_name = state
57
+ .pointer("/leader_receiver/window_name")
58
+ .and_then(Value::as_str)
59
+ .unwrap_or("leader");
56
60
  session_name.as_str().and_then(|session| {
57
61
  crate::tmux_backend::attach_command_for_workspace(
58
62
  workspace,
59
63
  &crate::transport::SessionName::new(session.to_string()),
60
- "leader",
64
+ window_name,
61
65
  )
62
66
  })
63
67
  };
@@ -86,6 +86,59 @@ use super::*;
86
86
  let _ = std::fs::remove_dir_all(&ws);
87
87
  }
88
88
 
89
+ #[test]
90
+ fn status_port_managed_attach_command_uses_receiver_window_name() {
91
+ let ws = seed_status_workspace();
92
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
93
+ if let Some(obj) = state.as_object_mut() {
94
+ obj.insert("is_external_leader".to_string(), json!(false));
95
+ obj.insert("session_name".to_string(), json!("team-current"));
96
+ obj.insert(
97
+ "leader_receiver".to_string(),
98
+ json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
99
+ );
100
+ }
101
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
102
+
103
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
104
+
105
+ let attach = v["leader_attach_command"]
106
+ .as_str()
107
+ .expect("managed status includes attach command");
108
+ assert!(attach.contains("attach -t team-current:claude_code"), "{attach}");
109
+ let _ = std::fs::remove_dir_all(&ws);
110
+ }
111
+
112
+ #[test]
113
+ fn leader_attach_command_for_plan_uses_plan_leader_window() {
114
+ let ws = tmp_workspace();
115
+ let plan = crate::leader::LeaderStartPlan {
116
+ mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
117
+ provider: crate::provider::Provider::ClaudeCode,
118
+ workspace: ws.clone(),
119
+ socket: crate::leader::LeaderLaunchSocket::Workspace,
120
+ session_name: Some(crate::transport::SessionName::new(
121
+ "team-agent-leader-claude_code-demo".to_string(),
122
+ )),
123
+ argv: Vec::new(),
124
+ provider_argv: Vec::new(),
125
+ leader_window: Some(crate::transport::WindowName::new("claude_code")),
126
+ is_external_leader: false,
127
+ leader_env: std::collections::BTreeMap::new(),
128
+ identity: None,
129
+ detached: false,
130
+ };
131
+
132
+ let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
133
+ .expect("managed plan has attach command");
134
+
135
+ assert!(
136
+ attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
137
+ "{attach}"
138
+ );
139
+ let _ = std::fs::remove_dir_all(&ws);
140
+ }
141
+
89
142
  #[test]
90
143
  fn status_port_missing_topology_marker_defaults_to_managed() {
91
144
  let ws = seed_status_workspace();
@@ -7,7 +7,9 @@ use std::process::{Command, Stdio};
7
7
 
8
8
  use crate::provider::{get_adapter, Provider};
9
9
  use crate::tmux_backend::TmuxBackend;
10
- use crate::transport::{PaneId, SessionName, SpawnResult, Target, Transport, WindowName};
10
+ use crate::transport::{
11
+ PaneId, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
12
+ };
11
13
 
12
14
  use super::helpers::{
13
15
  provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
@@ -405,6 +407,7 @@ fn execute_managed_leader_plan(
405
407
  .unwrap_or_else(|| "signal".to_string())
406
408
  )));
407
409
  }
410
+ ensure_managed_provider_live_after_attach(&transport, &spawned)?;
408
411
  Ok(LeaderLaunchOutcome {
409
412
  status: LeaderLaunchStatus::Exited,
410
413
  exit_code: code,
@@ -414,7 +417,7 @@ fn execute_managed_leader_plan(
414
417
  }
415
418
 
416
419
  fn ensure_managed_leader_pane(
417
- transport: &TmuxBackend,
420
+ transport: &dyn Transport,
418
421
  session: &SessionName,
419
422
  window: &WindowName,
420
423
  plan: &LeaderStartPlan,
@@ -455,6 +458,39 @@ fn ensure_managed_leader_pane(
455
458
  }
456
459
  }
457
460
 
461
+ fn ensure_managed_provider_live_after_attach(
462
+ transport: &dyn Transport,
463
+ spawned: &SpawnResult,
464
+ ) -> Result<(), LeaderError> {
465
+ let live = match transport.liveness(&spawned.pane_id) {
466
+ Ok(PaneLiveness::Live) => true,
467
+ Ok(PaneLiveness::Dead) => false,
468
+ Ok(PaneLiveness::Unknown) | Err(_) => managed_spawned_pane_in_targets(transport, spawned),
469
+ };
470
+ if live {
471
+ return Ok(());
472
+ }
473
+ Err(LeaderError::Start(format!(
474
+ "managed leader provider pane is not running after tmux client returned: {} {}:{}",
475
+ spawned.pane_id.as_str(),
476
+ spawned.session.as_str(),
477
+ spawned.window.as_str()
478
+ )))
479
+ }
480
+
481
+ fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnResult) -> bool {
482
+ transport
483
+ .list_targets()
484
+ .unwrap_or_default()
485
+ .iter()
486
+ .any(|pane| {
487
+ pane.pane_id.as_str() == spawned.pane_id.as_str()
488
+ && pane.session.as_str() == spawned.session.as_str()
489
+ && pane.window_name.as_ref().map(WindowName::as_str)
490
+ == Some(spawned.window.as_str())
491
+ })
492
+ }
493
+
458
494
  fn persist_managed_leader_binding(
459
495
  plan: &LeaderStartPlan,
460
496
  workspace: &Path,
@@ -911,11 +947,16 @@ mod tests {
911
947
  TurnVerification, WindowName,
912
948
  };
913
949
 
914
- use super::{execute_leader_plan, handle_exec_provider_startup_prompts, shlex_quote};
950
+ use super::{
951
+ ensure_managed_provider_live_after_attach, execute_leader_plan,
952
+ handle_exec_provider_startup_prompts, shlex_quote,
953
+ };
915
954
 
916
955
  struct ScriptedTransport {
917
956
  screens: Mutex<Vec<String>>,
918
957
  sent: Mutex<Vec<(Target, Vec<Key>)>>,
958
+ liveness: PaneLiveness,
959
+ targets: Vec<PaneInfo>,
919
960
  }
920
961
 
921
962
  impl ScriptedTransport {
@@ -923,6 +964,26 @@ mod tests {
923
964
  Self {
924
965
  screens: Mutex::new(screens),
925
966
  sent: Mutex::new(Vec::new()),
967
+ liveness: PaneLiveness::Unknown,
968
+ targets: Vec::new(),
969
+ }
970
+ }
971
+
972
+ fn with_liveness(liveness: PaneLiveness) -> Self {
973
+ Self {
974
+ screens: Mutex::new(Vec::new()),
975
+ sent: Mutex::new(Vec::new()),
976
+ liveness,
977
+ targets: Vec::new(),
978
+ }
979
+ }
980
+
981
+ fn with_liveness_and_targets(liveness: PaneLiveness, targets: Vec<PaneInfo>) -> Self {
982
+ Self {
983
+ screens: Mutex::new(Vec::new()),
984
+ sent: Mutex::new(Vec::new()),
985
+ liveness,
986
+ targets,
926
987
  }
927
988
  }
928
989
 
@@ -1024,11 +1085,11 @@ mod tests {
1024
1085
  }
1025
1086
 
1026
1087
  fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
1027
- Ok(PaneLiveness::Unknown)
1088
+ Ok(self.liveness)
1028
1089
  }
1029
1090
 
1030
1091
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
1031
- Ok(Vec::new())
1092
+ Ok(self.targets.clone())
1032
1093
  }
1033
1094
 
1034
1095
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
@@ -1063,6 +1124,69 @@ mod tests {
1063
1124
  }
1064
1125
  }
1065
1126
 
1127
+ fn managed_spawn_result() -> SpawnResult {
1128
+ SpawnResult {
1129
+ pane_id: PaneId::new("%42"),
1130
+ session: SessionName::new("team-agent-leader-claude_code-demo"),
1131
+ window: WindowName::new("claude_code"),
1132
+ child_pid: Some(1234),
1133
+ }
1134
+ }
1135
+
1136
+ fn managed_pane_info(spawned: &SpawnResult) -> PaneInfo {
1137
+ PaneInfo {
1138
+ pane_id: spawned.pane_id.clone(),
1139
+ session: spawned.session.clone(),
1140
+ window_index: Some(0),
1141
+ window_name: Some(spawned.window.clone()),
1142
+ pane_index: Some(0),
1143
+ tty: None,
1144
+ current_command: Some("claude".to_string()),
1145
+ current_path: None,
1146
+ active: true,
1147
+ pane_pid: spawned.child_pid,
1148
+ leader_env: BTreeMap::new(),
1149
+ }
1150
+ }
1151
+
1152
+ #[test]
1153
+ fn managed_attach_success_requires_live_provider_pane() {
1154
+ let spawned = managed_spawn_result();
1155
+ let transport = ScriptedTransport::with_liveness(PaneLiveness::Dead);
1156
+
1157
+ let err = ensure_managed_provider_live_after_attach(&transport, &spawned)
1158
+ .expect_err("dead provider pane must fail managed launch");
1159
+
1160
+ let text = err.to_string();
1161
+ assert!(
1162
+ text.contains("managed leader provider pane is not running"),
1163
+ "{text}"
1164
+ );
1165
+ assert!(text.contains("%42"), "{text}");
1166
+ assert!(text.contains("claude_code"), "{text}");
1167
+ }
1168
+
1169
+ #[test]
1170
+ fn managed_attach_success_accepts_live_provider_pane() {
1171
+ let spawned = managed_spawn_result();
1172
+ let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
1173
+
1174
+ ensure_managed_provider_live_after_attach(&transport, &spawned)
1175
+ .expect("live provider pane keeps managed launch successful");
1176
+ }
1177
+
1178
+ #[test]
1179
+ fn managed_attach_success_uses_target_scan_when_liveness_unknown() {
1180
+ let spawned = managed_spawn_result();
1181
+ let transport = ScriptedTransport::with_liveness_and_targets(
1182
+ PaneLiveness::Unknown,
1183
+ vec![managed_pane_info(&spawned)],
1184
+ );
1185
+
1186
+ ensure_managed_provider_live_after_attach(&transport, &spawned)
1187
+ .expect("target scan can prove provider pane is still live");
1188
+ }
1189
+
1066
1190
  struct EnvGuard {
1067
1191
  saved: Vec<(&'static str, Option<String>)>,
1068
1192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.34",
3
+ "version": "0.3.35",
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.34",
24
- "@team-agent/cli-darwin-x64": "0.3.34",
25
- "@team-agent/cli-linux-x64": "0.3.34"
23
+ "@team-agent/cli-darwin-arm64": "0.3.35",
24
+ "@team-agent/cli-darwin-x64": "0.3.35",
25
+ "@team-agent/cli-linux-x64": "0.3.35"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",