@team-agent/installer 0.3.34 → 0.3.36

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.36"
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.36"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -152,7 +152,7 @@ fn quickstart_human(value: &Value) -> String {
152
152
  .map(|items| items.iter().filter_map(Value::as_str).collect())
153
153
  .unwrap_or_default();
154
154
  if attach.is_empty() {
155
- return summary.to_string();
155
+ return append_reminder(summary.to_string(), crate::cli::QUICK_START_REMINDER);
156
156
  }
157
157
  let mut out = String::from(summary);
158
158
  out.push_str("\n\nattach:");
@@ -160,7 +160,15 @@ fn quickstart_human(value: &Value) -> String {
160
160
  out.push_str("\n ");
161
161
  out.push_str(cmd);
162
162
  }
163
- out
163
+ append_reminder(out, crate::cli::QUICK_START_REMINDER)
164
+ }
165
+
166
+ fn append_reminder(text: String, reminder: &str) -> String {
167
+ if text.is_empty() {
168
+ reminder.to_string()
169
+ } else {
170
+ format!("{text}\n{reminder}")
171
+ }
164
172
  }
165
173
 
166
174
  /// `cmd_compile`(`commands.py:42`)。
@@ -240,7 +248,10 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
240
248
  true,
241
249
  false,
242
250
  )?;
243
- return Ok(CmdResult::human(format_status_summary(&value)));
251
+ return Ok(CmdResult::human(append_reminder(
252
+ format_status_summary(&value),
253
+ crate::cli::STATUS_REMINDER,
254
+ )));
244
255
  }
245
256
  if args.json {
246
257
  let value = status_port::status_scoped(
@@ -252,12 +263,15 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
252
263
  )?;
253
264
  return Ok(CmdResult::from_json(value, true));
254
265
  }
255
- Ok(CmdResult::human(status_port::format_status_scoped(
256
- &selected.run_workspace,
257
- &selected.state,
258
- Some(&selected.team_key),
259
- args.agent.as_deref(),
260
- )?))
266
+ Ok(CmdResult::human(append_reminder(
267
+ status_port::format_status_scoped(
268
+ &selected.run_workspace,
269
+ &selected.state,
270
+ Some(&selected.team_key),
271
+ args.agent.as_deref(),
272
+ )?,
273
+ crate::cli::STATUS_REMINDER,
274
+ )))
261
275
  }
262
276
 
263
277
  fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
@@ -276,6 +290,7 @@ fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
276
290
  "action": "run `team-agent doctor` or inspect the log path shown here",
277
291
  "log": log_path.to_string_lossy().to_string(),
278
292
  "workspace": workspace.to_string_lossy().to_string(),
293
+ "reminder": crate::cli::STATUS_REMINDER,
279
294
  })
280
295
  }
281
296
 
@@ -1583,15 +1598,22 @@ mod tests {
1583
1598
  assert!(out.contains("team started"), "must keep summary; got {out}");
1584
1599
  assert!(out.contains("attach:"), "must render attach block; got {out}");
1585
1600
  assert!(out.contains("team-y:w1") && out.contains("team-y:w2"), "must list each attach cmd; got {out}");
1601
+ assert!(out.ends_with(crate::cli::QUICK_START_REMINDER), "must append harness reminder; got {out}");
1586
1602
  }
1587
1603
 
1588
1604
  #[test]
1589
1605
  fn e13_quickstart_human_summary_only_when_no_attach() {
1590
1606
  let value = json!({"summary": "quick-start complete"});
1591
- assert_eq!(quickstart_human(&value), "quick-start complete");
1607
+ assert_eq!(
1608
+ quickstart_human(&value),
1609
+ format!("quick-start complete\n{}", crate::cli::QUICK_START_REMINDER)
1610
+ );
1592
1611
  // 空数组也只 summary。
1593
1612
  let value2 = json!({"summary": "s", "attach_commands": []});
1594
- assert_eq!(quickstart_human(&value2), "s");
1613
+ assert_eq!(
1614
+ quickstart_human(&value2),
1615
+ format!("s\n{}", crate::cli::QUICK_START_REMINDER)
1616
+ );
1595
1617
  }
1596
1618
 
1597
1619
  #[test]
@@ -48,6 +48,9 @@ use crate::messaging::{self, AlertType, MessageTarget, SendOptions};
48
48
  use crate::model::ids::{TaskId, TeamKey};
49
49
 
50
50
  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)";
51
+ pub(crate) const QUICK_START_REMINDER: &str = "Reminder: Do not inspect raw worker terminal output during normal operation. Use team-agent status / inbox / collect instead. Wait for report_result.";
52
+ pub(crate) const SEND_REMINDER: &str = "Message delivered. Wait for the worker to report_result. Do not poll the worker terminal with capture-pane.";
53
+ pub(crate) const STATUS_REMINDER: &str = "To wait for results use --watch-result or team-agent collect. Do not capture-pane worker terminals.";
51
54
 
52
55
  pub mod adapters;
53
56
  pub mod diagnose;
@@ -158,13 +161,7 @@ pub mod lifecycle_port {
158
161
  crate::leader::LeaderLaunchStatus::Detached => true,
159
162
  crate::leader::LeaderLaunchStatus::NotStarted => false,
160
163
  };
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
- };
164
+ let leader_attach_command = leader_attach_command_for_plan(cwd, &plan);
168
165
  Ok(json!({
169
166
  "ok": ok,
170
167
  "provider": provider,
@@ -182,6 +179,18 @@ pub mod lifecycle_port {
182
179
  "session_name": plan.session_name.as_ref().map(|session| session.as_str().to_string()),
183
180
  }))
184
181
  }
182
+
183
+ pub(crate) fn leader_attach_command_for_plan(
184
+ cwd: &Path,
185
+ plan: &crate::leader::LeaderStartPlan,
186
+ ) -> Option<String> {
187
+ if plan.is_external_leader {
188
+ return None;
189
+ }
190
+ let session = plan.session_name.as_ref()?;
191
+ let window = plan.leader_window.as_ref()?;
192
+ crate::tmux_backend::attach_command_for_workspace(cwd, session, window.as_str())
193
+ }
185
194
  /// `runtime.shutdown`(`cmd_shutdown`)。
186
195
  pub fn shutdown(workspace: &Path, keep_logs: bool, team: Option<&str>) -> Result<Value, CliError> {
187
196
  let run_ws = crate::model::paths::canonical_run_workspace(workspace)
@@ -2522,6 +2531,7 @@ pub mod lifecycle_port {
2522
2531
  "display_backend": display_backend,
2523
2532
  "next_actions": next_actions,
2524
2533
  "attach_commands": attach_commands,
2534
+ "reminder": crate::cli::QUICK_START_REMINDER,
2525
2535
  "readiness": readiness_json.clone(),
2526
2536
  "worker_readiness": readiness_json,
2527
2537
  })
@@ -2540,6 +2550,7 @@ pub mod lifecycle_port {
2540
2550
  "state_path": state_path.map(|p| p.to_string_lossy().to_string()),
2541
2551
  "next_actions": next_actions,
2542
2552
  "attach_commands": attach_commands,
2553
+ "reminder": crate::cli::QUICK_START_REMINDER,
2543
2554
  }),
2544
2555
  crate::lifecycle::QuickStartReport::PreflightBlocked {
2545
2556
  summary,
@@ -2552,6 +2563,7 @@ pub mod lifecycle_port {
2552
2563
  "blockers": blockers,
2553
2564
  "next_actions": next_actions,
2554
2565
  "attach_commands": attach_commands,
2566
+ "reminder": crate::cli::QUICK_START_REMINDER,
2555
2567
  }),
2556
2568
  }
2557
2569
  }
@@ -2576,6 +2588,10 @@ pub mod lifecycle_port {
2576
2588
  Some("tmux -S /tmp/tmux-501/ta-test attach -t team-teamA:worker"),
2577
2589
  "B-2: ExistingRuntime JSON must preserve attach_commands instead of only next_actions; value={value}"
2578
2590
  );
2591
+ assert_eq!(
2592
+ value.get("reminder").and_then(Value::as_str),
2593
+ Some(crate::cli::QUICK_START_REMINDER)
2594
+ );
2579
2595
  }
2580
2596
 
2581
2597
  #[test]
@@ -2592,6 +2608,20 @@ pub mod lifecycle_port {
2592
2608
  "B-2: PreflightBlocked JSON must include attach_commands: [] for schema parity with Ready/Restart; value={value}"
2593
2609
  );
2594
2610
  }
2611
+
2612
+ #[test]
2613
+ fn restart_json_includes_harness_reminder() {
2614
+ let value = restart_value(crate::lifecycle::RestartReport::RefusedResumeAtomicity {
2615
+ unresumable: Vec::new(),
2616
+ allow_fresh: false,
2617
+ error: "resume refused".to_string(),
2618
+ });
2619
+
2620
+ assert_eq!(
2621
+ value.get("reminder").and_then(Value::as_str),
2622
+ Some(crate::cli::QUICK_START_REMINDER)
2623
+ );
2624
+ }
2595
2625
  }
2596
2626
 
2597
2627
  fn restart_value(report: crate::lifecycle::RestartReport) -> Value {
@@ -2610,6 +2640,7 @@ pub mod lifecycle_port {
2610
2640
  "coordinator_started": coordinator_started,
2611
2641
  "next_actions": next_actions,
2612
2642
  "attach_commands": attach_commands,
2643
+ "reminder": crate::cli::QUICK_START_REMINDER,
2613
2644
  }),
2614
2645
  crate::lifecycle::RestartReport::Partial {
2615
2646
  session_name,
@@ -2644,6 +2675,7 @@ pub mod lifecycle_port {
2644
2675
  "coordinator_started": coordinator_started,
2645
2676
  "next_actions": next_actions,
2646
2677
  "attach_commands": attach_commands,
2678
+ "reminder": crate::cli::QUICK_START_REMINDER,
2647
2679
  }),
2648
2680
  crate::lifecycle::RestartReport::Failed {
2649
2681
  session_name,
@@ -2675,6 +2707,7 @@ pub mod lifecycle_port {
2675
2707
  })).collect::<Vec<_>>(),
2676
2708
  "next_actions": next_actions,
2677
2709
  "attach_commands": attach_commands,
2710
+ "reminder": crate::cli::QUICK_START_REMINDER,
2678
2711
  }),
2679
2712
  crate::lifecycle::RestartReport::RefusedResumeAtomicity {
2680
2713
  unresumable,
@@ -2686,6 +2719,7 @@ pub mod lifecycle_port {
2686
2719
  "allow_fresh": allow_fresh,
2687
2720
  "error": error,
2688
2721
  "unresumable": unresumable.iter().map(|w| w.agent_id.as_str()).collect::<Vec<_>>(),
2722
+ "reminder": crate::cli::QUICK_START_REMINDER,
2689
2723
  }),
2690
2724
  crate::lifecycle::RestartReport::RefusedResumeNotReady {
2691
2725
  missing,
@@ -2710,6 +2744,7 @@ pub mod lifecycle_port {
2710
2744
  "pending_agent_ids": missing.iter().map(|w| w.as_str()).collect::<Vec<_>>(),
2711
2745
  },
2712
2746
  "next_action": "rerun restart after session capture completes, or pass --allow-fresh to deliberately discard missing context",
2747
+ "reminder": crate::cli::QUICK_START_REMINDER,
2713
2748
  }),
2714
2749
  crate::lifecycle::RestartReport::RefusedInvalidFirstSendAt {
2715
2750
  invalid,
@@ -2721,6 +2756,7 @@ pub mod lifecycle_port {
2721
2756
  "allow_fresh": allow_fresh,
2722
2757
  "error": error,
2723
2758
  "invalid": invalid.iter().map(|w| w.worker_id.as_str()).collect::<Vec<_>>(),
2759
+ "reminder": crate::cli::QUICK_START_REMINDER,
2724
2760
  }),
2725
2761
  }
2726
2762
  }
@@ -22,7 +22,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
22
22
  if content.is_empty() {
23
23
  return Err(CliError::Usage("--pane requires a non-empty message".to_string()));
24
24
  }
25
- let value = send_to_pane_direct(
25
+ let mut value = send_to_pane_direct(
26
26
  &args.workspace,
27
27
  pane_id,
28
28
  &content,
@@ -31,6 +31,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
31
31
  args.team.as_deref(),
32
32
  args.json,
33
33
  )?;
34
+ add_send_reminder_if_ok(&mut value);
34
35
  return Ok(CmdResult::from_json(value, args.json));
35
36
  }
36
37
  let selected = crate::state::selector::resolve_active_team(
@@ -60,6 +61,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
60
61
  obj.insert("watch".to_string(), watch_notice_json(&target, &opts));
61
62
  }
62
63
  }
64
+ add_send_reminder_if_ok(&mut value);
63
65
  Ok(CmdResult::from_json(value, args.json))
64
66
  }
65
67
 
@@ -471,6 +473,15 @@ fn delivery_outcome_json(
471
473
  })
472
474
  }
473
475
 
476
+ fn add_send_reminder_if_ok(value: &mut Value) {
477
+ if value.get("ok").and_then(Value::as_bool) != Some(true) {
478
+ return;
479
+ }
480
+ if let Some(obj) = value.as_object_mut() {
481
+ obj.insert("reminder".to_string(), json!(crate::cli::SEND_REMINDER));
482
+ }
483
+ }
484
+
474
485
  fn target_json(target: &MessageTarget) -> Value {
475
486
  match target {
476
487
  MessageTarget::Single(agent) => json!(agent),
@@ -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
  };
@@ -95,6 +99,7 @@ use rusqlite::params;
95
99
  "readiness": readiness,
96
100
  "coordinator": coordinator_health_value(health),
97
101
  "runtime": runtime_block,
102
+ "reminder": crate::cli::STATUS_REMINDER,
98
103
  "last_events": Value::Array(
99
104
  crate::event_log::EventLog::new(workspace)
100
105
  .tail(10)
@@ -594,6 +599,7 @@ use rusqlite::params;
594
599
  "latest_results": take_array(full.get("latest_results"), 5),
595
600
  "readiness": full.get("readiness").cloned().unwrap_or_else(|| json!({})),
596
601
  "coordinator": compact_object(full.get("coordinator"), &["status", "pid", "metadata_ok", "schema_ok"]),
602
+ "reminder": full.get("reminder").cloned().unwrap_or_else(|| json!(crate::cli::STATUS_REMINDER)),
597
603
  "last_events": take_array_tail(full.get("last_events"), 10),
598
604
  })
599
605
  }
@@ -50,10 +50,15 @@ use super::*;
50
50
  "results",
51
51
  "latest_results",
52
52
  "coordinator",
53
+ "reminder",
53
54
  "last_events",
54
55
  ] {
55
56
  assert!(obj.contains_key(key), "compact status missing key `{key}`");
56
57
  }
58
+ assert_eq!(
59
+ obj.get("reminder").and_then(|v| v.as_str()),
60
+ Some(crate::cli::STATUS_REMINDER)
61
+ );
57
62
  // seeded agent surfaces through the projection.
58
63
  assert!(
59
64
  obj["agents"].as_object().unwrap().contains_key("a1"),
@@ -65,6 +70,27 @@ use super::*;
65
70
  let _ = std::fs::remove_dir_all(&ws);
66
71
  }
67
72
 
73
+ #[test]
74
+ fn cmd_status_human_appends_harness_reminder() {
75
+ let ws = seed_status_workspace();
76
+ let args = StatusArgs {
77
+ agent: None,
78
+ workspace: ws.clone(),
79
+ detail: false,
80
+ summary: false,
81
+ json: false,
82
+ };
83
+
84
+ let r = cmd_status_for_team(&args, None).expect("status");
85
+ let text = match r.output {
86
+ CmdOutput::Human(text) => text,
87
+ other => panic!("expected human status output, got {other:?}"),
88
+ };
89
+
90
+ assert!(text.ends_with(crate::cli::STATUS_REMINDER), "{text}");
91
+ let _ = std::fs::remove_dir_all(&ws);
92
+ }
93
+
68
94
  #[test]
69
95
  fn status_port_status_reports_managed_leader_topology_and_attach_command() {
70
96
  let ws = seed_status_workspace();
@@ -86,6 +112,59 @@ use super::*;
86
112
  let _ = std::fs::remove_dir_all(&ws);
87
113
  }
88
114
 
115
+ #[test]
116
+ fn status_port_managed_attach_command_uses_receiver_window_name() {
117
+ let ws = seed_status_workspace();
118
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
119
+ if let Some(obj) = state.as_object_mut() {
120
+ obj.insert("is_external_leader".to_string(), json!(false));
121
+ obj.insert("session_name".to_string(), json!("team-current"));
122
+ obj.insert(
123
+ "leader_receiver".to_string(),
124
+ json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
125
+ );
126
+ }
127
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
128
+
129
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
130
+
131
+ let attach = v["leader_attach_command"]
132
+ .as_str()
133
+ .expect("managed status includes attach command");
134
+ assert!(attach.contains("attach -t team-current:claude_code"), "{attach}");
135
+ let _ = std::fs::remove_dir_all(&ws);
136
+ }
137
+
138
+ #[test]
139
+ fn leader_attach_command_for_plan_uses_plan_leader_window() {
140
+ let ws = tmp_workspace();
141
+ let plan = crate::leader::LeaderStartPlan {
142
+ mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
143
+ provider: crate::provider::Provider::ClaudeCode,
144
+ workspace: ws.clone(),
145
+ socket: crate::leader::LeaderLaunchSocket::Workspace,
146
+ session_name: Some(crate::transport::SessionName::new(
147
+ "team-agent-leader-claude_code-demo".to_string(),
148
+ )),
149
+ argv: Vec::new(),
150
+ provider_argv: Vec::new(),
151
+ leader_window: Some(crate::transport::WindowName::new("claude_code")),
152
+ is_external_leader: false,
153
+ leader_env: std::collections::BTreeMap::new(),
154
+ identity: None,
155
+ detached: false,
156
+ };
157
+
158
+ let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
159
+ .expect("managed plan has attach command");
160
+
161
+ assert!(
162
+ attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
163
+ "{attach}"
164
+ );
165
+ let _ = std::fs::remove_dir_all(&ws);
166
+ }
167
+
89
168
  #[test]
90
169
  fn status_port_missing_topology_marker_defaults_to_managed() {
91
170
  let ws = seed_status_workspace();
@@ -202,6 +281,12 @@ use super::*;
202
281
  match r.output {
203
282
  CmdOutput::Json(ref v) => {
204
283
  assert!(v.get("ok").is_some(), "send result Json must carry `ok`");
284
+ if v.get("ok").and_then(|ok| ok.as_bool()) == Some(true) {
285
+ assert_eq!(
286
+ v.get("reminder").and_then(|reminder| reminder.as_str()),
287
+ Some(crate::cli::SEND_REMINDER)
288
+ );
289
+ }
205
290
  }
206
291
  other => panic!("cmd_send must emit Json DeliveryOutcome, got {other:?}"),
207
292
  }
@@ -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.36",
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.36",
24
+ "@team-agent/cli-darwin-x64": "0.3.36",
25
+ "@team-agent/cli-linux-x64": "0.3.36"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",