@team-agent/installer 0.3.2 → 0.3.4

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 (82) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +145 -11
  6. package/crates/team-agent/src/cli/emit.rs +287 -53
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +807 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler/tests.rs +2 -2
  16. package/crates/team-agent/src/compiler.rs +16 -6
  17. package/crates/team-agent/src/coordinator/health.rs +89 -20
  18. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  19. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  20. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  21. package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
  22. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  23. package/crates/team-agent/src/coordinator/types.rs +15 -3
  24. package/crates/team-agent/src/db/schema.rs +37 -2
  25. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  26. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  27. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  28. package/crates/team-agent/src/fake_worker.rs +146 -3
  29. package/crates/team-agent/src/leader/start.rs +121 -23
  30. package/crates/team-agent/src/leader/types.rs +44 -1
  31. package/crates/team-agent/src/lib.rs +3 -0
  32. package/crates/team-agent/src/lifecycle/display.rs +648 -50
  33. package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
  34. package/crates/team-agent/src/lifecycle/mod.rs +3 -0
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  36. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  37. package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
  38. package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
  40. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  41. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  42. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
  44. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  45. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
  46. package/crates/team-agent/src/lifecycle/types.rs +23 -0
  47. package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
  48. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  50. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  51. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  52. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  53. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  54. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  55. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  56. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  57. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  58. package/crates/team-agent/src/message_store.rs +21 -4
  59. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  60. package/crates/team-agent/src/messaging/mod.rs +9 -6
  61. package/crates/team-agent/src/messaging/results.rs +153 -16
  62. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  63. package/crates/team-agent/src/messaging/send.rs +35 -3
  64. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  65. package/crates/team-agent/src/messaging/types.rs +11 -3
  66. package/crates/team-agent/src/os_probe.rs +119 -0
  67. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  68. package/crates/team-agent/src/packaging/tests.rs +23 -0
  69. package/crates/team-agent/src/provider/adapter.rs +483 -67
  70. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  71. package/crates/team-agent/src/provider/classify.rs +51 -4
  72. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  73. package/crates/team-agent/src/provider/types.rs +47 -0
  74. package/crates/team-agent/src/session_capture.rs +616 -0
  75. package/crates/team-agent/src/state/persist.rs +57 -0
  76. package/crates/team-agent/src/state/projection.rs +32 -23
  77. package/crates/team-agent/src/state/selector.rs +5 -2
  78. package/crates/team-agent/src/tmux_backend.rs +151 -60
  79. package/crates/team-agent/src/transport/test_support.rs +9 -0
  80. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  81. package/crates/team-agent/src/transport.rs +13 -2
  82. package/package.json +4 -4
@@ -19,6 +19,13 @@ use crate::state::persist::{load_runtime_state, save_runtime_state_with_deleted_
19
19
  /// `team_state_key`(`state.py:93`):从 team_dir(.name)/spec_path(.parent.name)派生 team key,
20
20
  /// 跳过 `.team`/`runtime`;兜底 `session_name` 或 `"current"`。
21
21
  pub fn team_state_key(state: &Value) -> String {
22
+ if let Some(team_key) = state
23
+ .get("team_key")
24
+ .and_then(Value::as_str)
25
+ .filter(|key| !key.is_empty())
26
+ {
27
+ return team_key.to_string();
28
+ }
22
29
  for field in ["team_dir", "spec_path"] {
23
30
  // Python `if not value: continue` —— None/空串 falsy 跳过。
24
31
  let value = match state.get(field).and_then(Value::as_str) {
@@ -68,9 +75,7 @@ pub fn resolve_owner_team_id(state: &Value, owner_team_id: &str) -> OwnerTeamRes
68
75
  }
69
76
  let teams = state.get("teams").and_then(Value::as_object);
70
77
  if teams.is_some_and(|teams| teams.contains_key(requested)) {
71
- if has_top_level_runtime_content(state) {
72
- return OwnerTeamResolution::Canonical(requested.to_string());
73
- }
78
+ return OwnerTeamResolution::Canonical(requested.to_string());
74
79
  }
75
80
  if teams.is_none_or(Map::is_empty) {
76
81
  let active = state.get("active_team_key").and_then(Value::as_str).unwrap_or("");
@@ -113,21 +118,6 @@ pub fn resolve_owner_team_id(state: &Value, owner_team_id: &str) -> OwnerTeamRes
113
118
  }
114
119
  }
115
120
 
116
- fn has_top_level_runtime_content(state: &Value) -> bool {
117
- [
118
- "session_name",
119
- "team_dir",
120
- "spec_path",
121
- "workspace",
122
- "agents",
123
- "tasks",
124
- "leader_receiver",
125
- "team_owner",
126
- ]
127
- .into_iter()
128
- .any(|key| state.get(key).is_some_and(super::json_truthy))
129
- }
130
-
131
121
  fn legacy_owner_team_aliases(entry: &Value) -> impl Iterator<Item = String> + '_ {
132
122
  let scalar_paths = [
133
123
  "/team/name",
@@ -357,11 +347,7 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
357
347
  }
358
348
  let matches: Vec<&String> = alive
359
349
  .iter()
360
- .filter(|(key, value)| {
361
- let session = value.get("session_name").and_then(Value::as_str).unwrap_or("");
362
- let dir = value.get("team_dir").and_then(Value::as_str).unwrap_or("");
363
- team == key.as_str() || team == session || team == dir
364
- })
350
+ .filter(|(key, value)| team_selector_matches(team, key, value))
365
351
  .map(|(k, _)| k)
366
352
  .collect();
367
353
  if matches.len() == 1 {
@@ -398,6 +384,29 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
398
384
  ))
399
385
  }
400
386
 
387
+ fn team_selector_matches(team: &str, key: &str, value: &Value) -> bool {
388
+ if team == key {
389
+ return true;
390
+ }
391
+ let session = value.get("session_name").and_then(Value::as_str).unwrap_or("");
392
+ if team == session {
393
+ return true;
394
+ }
395
+ if let Some(stripped) = session.strip_prefix("team-") {
396
+ if team == stripped {
397
+ return true;
398
+ }
399
+ }
400
+ let dir = value.get("team_dir").and_then(Value::as_str).unwrap_or("");
401
+ if team == dir {
402
+ return true;
403
+ }
404
+ std::path::Path::new(dir)
405
+ .file_name()
406
+ .and_then(|name| name.to_str())
407
+ .is_some_and(|name| team == name)
408
+ }
409
+
401
410
  /// `ambiguous_team_target_result`(`state.py:226`):无显式 team 且多候选 → 拒绝 dict;否则 None。
402
411
  pub fn ambiguous_team_target_result(state: &Value) -> Option<Value> {
403
412
  let alive = team_state_candidates(state);
@@ -98,8 +98,11 @@ fn spec_workspace_from_state(state: &Value) -> Option<PathBuf> {
98
98
  }
99
99
 
100
100
  fn selected_team_key(state: &Value, team: Option<&str>) -> String {
101
- team.filter(|s| !s.is_empty())
101
+ state
102
+ .get("active_team_key")
103
+ .and_then(Value::as_str)
104
+ .filter(|s| !s.is_empty())
102
105
  .map(ToString::to_string)
103
- .or_else(|| state.get("active_team_key").and_then(Value::as_str).filter(|s| !s.is_empty()).map(ToString::to_string))
106
+ .or_else(|| team.filter(|s| !s.is_empty()).map(ToString::to_string))
104
107
  .unwrap_or_else(|| team_state_key(state))
105
108
  }
@@ -27,8 +27,8 @@ use crate::transport::{
27
27
  normalize_capture, tmux_capture_argv, tmux_empty_inject_argv, tmux_inject_text_argv,
28
28
  tmux_query_argv, tmux_send_keys_argv, tmux_spawn_argv, AttachOutcome, BackendKind,
29
29
  CaptureRange, CapturedText, InjectPayload, InjectReport, InjectStage, InjectVerification, Key,
30
- PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome, SpawnResult, SubmitVerification,
31
- Target, Transport, TransportError, TurnVerification, WindowName,
30
+ PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome, SpawnResult,
31
+ SubmitVerification, Target, Transport, TransportError, TurnVerification, WindowName,
32
32
  };
33
33
 
34
34
  /// Result of running an external command — the typed output of the OS edge.
@@ -91,22 +91,29 @@ impl RealCommandRunner {
91
91
  };
92
92
  let mut child = std::process::Command::new(program)
93
93
  .args(argv.iter().skip(1))
94
- .stdin(if stdin_text.is_some() { Stdio::piped() } else { Stdio::null() })
94
+ .stdin(if stdin_text.is_some() {
95
+ Stdio::piped()
96
+ } else {
97
+ Stdio::null()
98
+ })
95
99
  .stdout(Stdio::piped())
96
100
  .stderr(Stdio::piped())
97
101
  .spawn()?;
98
102
  if let Some(text) = stdin_text {
99
- let mut stdin = child.stdin.take().ok_or_else(|| {
100
- std::io::Error::other("stdin pipe missing")
101
- })?;
103
+ let mut stdin = child
104
+ .stdin
105
+ .take()
106
+ .ok_or_else(|| std::io::Error::other("stdin pipe missing"))?;
102
107
  stdin.write_all(text.as_bytes())?;
103
108
  }
104
- let stdout = child.stdout.take().ok_or_else(|| {
105
- std::io::Error::other("stdout pipe missing")
106
- })?;
107
- let stderr = child.stderr.take().ok_or_else(|| {
108
- std::io::Error::other("stderr pipe missing")
109
- })?;
109
+ let stdout = child
110
+ .stdout
111
+ .take()
112
+ .ok_or_else(|| std::io::Error::other("stdout pipe missing"))?;
113
+ let stderr = child
114
+ .stderr
115
+ .take()
116
+ .ok_or_else(|| std::io::Error::other("stderr pipe missing"))?;
110
117
  let stdout_thread = std::thread::spawn(move || read_pipe(stdout));
111
118
  let stderr_thread = std::thread::spawn(move || read_pipe(stderr));
112
119
  let deadline = Instant::now() + COMMAND_TIMEOUT;
@@ -173,7 +180,10 @@ impl TmuxBackend {
173
180
  /// Backend bound to the real `tmux` subprocess on the SHARED default socket (no `-L`).
174
181
  /// Non-team callers + existing argv/unit tests stay unaffected.
175
182
  pub fn new() -> Self {
176
- Self { runner: Box::new(RealCommandRunner), socket: None }
183
+ Self {
184
+ runner: Box::new(RealCommandRunner),
185
+ socket: None,
186
+ }
177
187
  }
178
188
 
179
189
  /// CP-1 team backend: bound to the real `tmux` subprocess on a PER-WORKSPACE socket, derived
@@ -182,7 +192,9 @@ impl TmuxBackend {
182
192
  pub fn for_workspace(workspace: &Path) -> Self {
183
193
  Self {
184
194
  runner: Box::new(RealCommandRunner),
185
- socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))),
195
+ socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(
196
+ workspace,
197
+ ))),
186
198
  }
187
199
  }
188
200
 
@@ -190,7 +202,10 @@ impl TmuxBackend {
190
202
  if socket.is_empty() || socket == "default" {
191
203
  Self::new()
192
204
  } else {
193
- Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Name(socket.to_string())) }
205
+ Self {
206
+ runner: Box::new(RealCommandRunner),
207
+ socket: Some(TmuxSocketEndpoint::Name(socket.to_string())),
208
+ }
194
209
  }
195
210
  }
196
211
 
@@ -198,7 +213,10 @@ impl TmuxBackend {
198
213
  if endpoint.is_empty() || endpoint == "default" {
199
214
  Self::new()
200
215
  } else if Path::new(endpoint).is_absolute() {
201
- Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
216
+ Self {
217
+ runner: Box::new(RealCommandRunner),
218
+ socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())),
219
+ }
202
220
  } else {
203
221
  Self::new()
204
222
  }
@@ -206,25 +224,50 @@ impl TmuxBackend {
206
224
 
207
225
  /// Backend with an injected runner (tests: canned/recording tmux output). Shared default socket.
208
226
  pub fn with_runner(runner: Box<dyn CommandRunner>) -> Self {
209
- Self { runner, socket: None }
227
+ Self {
228
+ runner,
229
+ socket: None,
230
+ }
210
231
  }
211
232
 
212
233
  /// Backend with an injected runner bound to a per-workspace socket (tests: assert the `-L` is in
213
234
  /// the recorded argv for a workspace-bound backend).
214
235
  pub fn with_runner_for_workspace(runner: Box<dyn CommandRunner>, workspace: &Path) -> Self {
215
- Self { runner, socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))) }
236
+ Self {
237
+ runner,
238
+ socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(
239
+ workspace,
240
+ ))),
241
+ }
216
242
  }
217
243
 
218
- pub(crate) fn with_runner_for_tmux_endpoint(runner: Box<dyn CommandRunner>, endpoint: &str) -> Self {
244
+ pub(crate) fn with_runner_for_tmux_endpoint(
245
+ runner: Box<dyn CommandRunner>,
246
+ endpoint: &str,
247
+ ) -> Self {
219
248
  if Path::new(endpoint).is_absolute() {
220
- Self { runner, socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
249
+ Self {
250
+ runner,
251
+ socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())),
252
+ }
221
253
  } else if endpoint.is_empty() || endpoint == "default" {
222
- Self { runner, socket: None }
254
+ Self {
255
+ runner,
256
+ socket: None,
257
+ }
223
258
  } else {
224
- Self { runner, socket: None }
259
+ Self {
260
+ runner,
261
+ socket: None,
262
+ }
225
263
  }
226
264
  }
227
265
 
266
+ /// Build the exact argv that a workspace-bound tmux backend will execute.
267
+ pub fn argv_for_workspace(workspace: &Path, argv: &[String]) -> Vec<String> {
268
+ Self::for_workspace(workspace).tmux_argv(argv)
269
+ }
270
+
228
271
  /// THE RUN CHOKEPOINT: every executed `tmux` argv is funneled through here. When a per-team
229
272
  /// socket is set, inject `-L <socket>` right after the leading "tmux" token; otherwise pass argv
230
273
  /// through unchanged. Non-`tmux` argv (e.g. the spawned provider command) is never rewritten.
@@ -257,10 +300,7 @@ impl TmuxBackend {
257
300
  if self.socket.is_none() {
258
301
  return;
259
302
  }
260
- let argv = self.tmux_argv(&[
261
- "tmux".to_string(),
262
- "kill-server".to_string(),
263
- ]);
303
+ let argv = self.tmux_argv(&["tmux".to_string(), "kill-server".to_string()]);
264
304
  let _ = self.runner.run(&argv);
265
305
  }
266
306
  }
@@ -278,6 +318,60 @@ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
278
318
  format!("ta-{:012x}", hasher.finish() & 0xffff_ffff_ffff)
279
319
  }
280
320
 
321
+ pub(crate) fn socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
322
+ let socket_name = socket_name_for_workspace(workspace);
323
+ let roots = tmux_socket_roots();
324
+ for root in &roots {
325
+ let root = root.canonicalize().unwrap_or_else(|_| root.clone());
326
+ let candidate = root.join(&socket_name);
327
+ if candidate.exists() {
328
+ return Some(candidate.canonicalize().unwrap_or(candidate));
329
+ }
330
+ }
331
+ let uid = unsafe { libc::geteuid() };
332
+ let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
333
+ let default_root = default_root
334
+ .canonicalize()
335
+ .unwrap_or(default_root);
336
+ Some(default_root.join(socket_name))
337
+ }
338
+
339
+ pub(crate) fn attach_command_for_workspace(
340
+ workspace: &Path,
341
+ session_name: &SessionName,
342
+ window_name: &str,
343
+ ) -> Option<String> {
344
+ let socket_path = socket_path_for_workspace(workspace)?;
345
+ Some(format!(
346
+ "tmux -S {} attach -t {}:{}",
347
+ socket_path.display(),
348
+ session_name.as_str(),
349
+ window_name
350
+ ))
351
+ }
352
+
353
+ pub(crate) fn attach_commands_for_windows<'a>(
354
+ workspace: &Path,
355
+ session_name: &SessionName,
356
+ window_names: impl IntoIterator<Item = &'a str>,
357
+ ) -> Vec<String> {
358
+ window_names
359
+ .into_iter()
360
+ .filter_map(|window_name| attach_command_for_workspace(workspace, session_name, window_name))
361
+ .collect()
362
+ }
363
+
364
+ fn tmux_socket_roots() -> Vec<PathBuf> {
365
+ let uid = unsafe { libc::geteuid() };
366
+ let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
367
+ if let Some(tmpdir) = std::env::var_os("TMPDIR") {
368
+ roots.push(PathBuf::from(tmpdir).join(format!("tmux-{uid}")));
369
+ }
370
+ roots.sort();
371
+ roots.dedup();
372
+ roots
373
+ }
374
+
281
375
  pub(crate) fn socket_name_from_tmux_env() -> Option<String> {
282
376
  let tmux = std::env::var("TMUX")
283
377
  .ok()
@@ -362,10 +456,13 @@ impl TmuxBackend {
362
456
 
363
457
  fn run_spawn(&self, argv: &[String]) -> Result<CommandOutput, TransportError> {
364
458
  let argv = self.tmux_argv(argv);
365
- let output = self.runner.run(&argv).map_err(|source| TransportError::Spawn {
366
- backend: BackendKind::Tmux,
367
- source,
368
- })?;
459
+ let output = self
460
+ .runner
461
+ .run(&argv)
462
+ .map_err(|source| TransportError::Spawn {
463
+ backend: BackendKind::Tmux,
464
+ source,
465
+ })?;
369
466
  if output.success {
370
467
  Ok(output)
371
468
  } else {
@@ -373,16 +470,12 @@ impl TmuxBackend {
373
470
  }
374
471
  }
375
472
 
376
- fn run_inject_stage(
377
- &self,
378
- argv: &[String],
379
- stage: InjectStage,
380
- ) -> Result<(), TransportError> {
473
+ fn run_inject_stage(&self, argv: &[String], stage: InjectStage) -> Result<(), TransportError> {
381
474
  let argv = self.tmux_argv(argv);
382
- let output = self.runner.run(&argv).map_err(|source| TransportError::Inject {
383
- stage,
384
- source,
385
- })?;
475
+ let output = self
476
+ .runner
477
+ .run(&argv)
478
+ .map_err(|source| TransportError::Inject { stage, source })?;
386
479
  if output.success {
387
480
  Ok(())
388
481
  } else {
@@ -397,10 +490,10 @@ impl TmuxBackend {
397
490
  stdin: &str,
398
491
  ) -> Result<(), TransportError> {
399
492
  let argv = self.tmux_argv(argv);
400
- let output = self.runner.run_with_stdin(&argv, stdin).map_err(|source| TransportError::Inject {
401
- stage,
402
- source,
403
- })?;
493
+ let output = self
494
+ .runner
495
+ .run_with_stdin(&argv, stdin)
496
+ .map_err(|source| TransportError::Inject { stage, source })?;
404
497
  if output.success {
405
498
  Ok(())
406
499
  } else {
@@ -611,11 +704,12 @@ impl Transport for TmuxBackend {
611
704
  }
612
705
  return Ok(InjectReport {
613
706
  stage_reached: InjectStage::Submit,
614
- inject_verification: InjectVerification::CaptureContainsNewPastedContentPrompt,
707
+ inject_verification:
708
+ InjectVerification::CaptureContainsNewPastedContentPrompt,
615
709
  submit_verification: if cleared {
616
710
  SubmitVerification::PastedContentPromptAbsentAfterSubmit
617
711
  } else {
618
- submit_verification_for_key(submit)
712
+ SubmitVerification::PastedContentPromptStillPresentAfterSubmit
619
713
  },
620
714
  turn_verification: TurnVerification::NotYetObserved,
621
715
  attempts,
@@ -656,7 +750,10 @@ impl Transport for TmuxBackend {
656
750
  ) -> Result<CapturedText, TransportError> {
657
751
  let pane = pane_from_target(target);
658
752
  let argv = self.tmux_argv(&tmux_capture_argv(&pane, range));
659
- let output = self.runner.run(&argv).map_err(|source| TransportError::Capture { source })?;
753
+ let output = self
754
+ .runner
755
+ .run(&argv)
756
+ .map_err(|source| TransportError::Capture { source })?;
660
757
  if !output.success {
661
758
  return Err(subprocess_error(argv, output));
662
759
  }
@@ -666,11 +763,7 @@ impl Transport for TmuxBackend {
666
763
  })
667
764
  }
668
765
 
669
- fn query(
670
- &self,
671
- target: &Target,
672
- field: PaneField,
673
- ) -> Result<Option<String>, TransportError> {
766
+ fn query(&self, target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
674
767
  let pane = pane_from_target(target);
675
768
  let argv = self.tmux_argv(&tmux_query_argv(&pane, field));
676
769
  let output = self.runner.run(&argv)?;
@@ -693,7 +786,11 @@ impl Transport for TmuxBackend {
693
786
  if output.success {
694
787
  return Ok(PaneLiveness::Live);
695
788
  }
696
- if output.stderr.to_ascii_lowercase().contains("can't find pane") {
789
+ if output
790
+ .stderr
791
+ .to_ascii_lowercase()
792
+ .contains("can't find pane")
793
+ {
697
794
  Ok(PaneLiveness::Dead)
698
795
  } else {
699
796
  Ok(PaneLiveness::Unknown)
@@ -736,10 +833,7 @@ impl Transport for TmuxBackend {
736
833
  Ok(output.success)
737
834
  }
738
835
 
739
- fn list_windows(
740
- &self,
741
- session: &SessionName,
742
- ) -> Result<Vec<WindowName>, TransportError> {
836
+ fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
743
837
  // golden runtime.py:1023-1029 `_tmux_window_exists`: `tmux list-windows -t <s> -F #{window_name}`;
744
838
  // returncode != 0 -> false (here: an empty window set), else the window names by line.
745
839
  let argv = self.tmux_argv(&[
@@ -800,10 +894,7 @@ impl Transport for TmuxBackend {
800
894
  self.run_ok(&argv)
801
895
  }
802
896
 
803
- fn attach_session(
804
- &self,
805
- session: &SessionName,
806
- ) -> Result<AttachOutcome, TransportError> {
897
+ fn attach_session(&self, session: &SessionName) -> Result<AttachOutcome, TransportError> {
807
898
  let argv = [
808
899
  "tmux".to_string(),
809
900
  "attach-session".to_string(),
@@ -21,6 +21,7 @@ pub struct SpawnRecord {
21
21
  #[derive(Debug, Clone, Default)]
22
22
  struct OfflineState {
23
23
  session_present: bool,
24
+ session_absent_after_spawn_first: bool,
24
25
  targets: Vec<PaneInfo>,
25
26
  windows: Vec<WindowName>,
26
27
  calls: Vec<&'static str>,
@@ -44,6 +45,11 @@ impl OfflineTransport {
44
45
  self
45
46
  }
46
47
 
48
+ pub fn with_session_absent_after_spawn_first(self) -> Self {
49
+ self.with_state(|state| state.session_absent_after_spawn_first = true);
50
+ self
51
+ }
52
+
47
53
  pub fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
48
54
  self.with_state(|state| state.targets = targets);
49
55
  self
@@ -100,6 +106,9 @@ impl OfflineTransport {
100
106
  let pane_index = self.with_state(|state| {
101
107
  state.calls.push(kind);
102
108
  state.spawns.push(SpawnRecord { kind: kind.to_string(), argv: argv.to_vec() });
109
+ if kind == "spawn_first" && !state.session_absent_after_spawn_first {
110
+ state.session_present = true;
111
+ }
103
112
  state.spawns.len().saturating_sub(1)
104
113
  });
105
114
  SpawnResult {
@@ -103,6 +103,10 @@
103
103
  submit_verification_wire(SubmitVerification::PastedContentPromptAbsentAfterSubmit),
104
104
  "pasted_content_prompt_absent_after_submit"
105
105
  );
106
+ assert_eq!(
107
+ submit_verification_wire(SubmitVerification::PastedContentPromptStillPresentAfterSubmit),
108
+ "pasted_content_prompt_still_present_after_submit"
109
+ );
106
110
  assert_eq!(
107
111
  submit_verification_wire(SubmitVerification::SendKeysFailed),
108
112
  "send_keys_failed"
@@ -165,6 +165,7 @@ pub enum Key {
165
165
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
166
166
  pub enum CaptureRange {
167
167
  Tail(u32),
168
+ Head(u32),
168
169
  Full,
169
170
  }
170
171
 
@@ -290,6 +291,8 @@ pub enum SubmitVerification {
290
291
  EnterSentWithoutPlaceholderCheck,
291
292
  /// `pasted_content_prompt_absent_after_submit`。
292
293
  PastedContentPromptAbsentAfterSubmit,
294
+ /// `pasted_content_prompt_still_present_after_submit`。
295
+ PastedContentPromptStillPresentAfterSubmit,
293
296
  /// `{key}_sent_after_visible_token`(key 由 variant 携带)。
294
297
  KeySentAfterVisibleToken { key: Key },
295
298
  /// `send_keys_failed`。
@@ -580,9 +583,10 @@ pub fn tmux_cancel_mode_argv(pane: &PaneId, mode: PaneMode) -> Vec<String> {
580
583
  pub fn tmux_capture_argv(pane: &PaneId, range: CaptureRange) -> Vec<String> {
581
584
  let spec = match range {
582
585
  CaptureRange::Tail(lines) => format!("-{lines}"),
586
+ CaptureRange::Head(_) => "0".to_string(),
583
587
  CaptureRange::Full => "-".to_string(),
584
588
  };
585
- vec![
589
+ let mut argv = vec![
586
590
  "tmux".to_string(),
587
591
  "capture-pane".to_string(),
588
592
  "-p".to_string(),
@@ -590,7 +594,11 @@ pub fn tmux_capture_argv(pane: &PaneId, range: CaptureRange) -> Vec<String> {
590
594
  spec,
591
595
  "-t".to_string(),
592
596
  pane.as_str().to_string(),
593
- ]
597
+ ];
598
+ if let CaptureRange::Head(lines) = range {
599
+ argv.extend(["-E".to_string(), lines.saturating_sub(1).to_string()]);
600
+ }
601
+ argv
594
602
  }
595
603
 
596
604
  /// PaneField → `display-message -p -t <target> [-F] <fmt>`。
@@ -748,6 +756,9 @@ pub fn submit_verification_wire(v: SubmitVerification) -> String {
748
756
  SubmitVerification::PastedContentPromptAbsentAfterSubmit => {
749
757
  "pasted_content_prompt_absent_after_submit".to_string()
750
758
  }
759
+ SubmitVerification::PastedContentPromptStillPresentAfterSubmit => {
760
+ "pasted_content_prompt_still_present_after_submit".to_string()
761
+ }
751
762
  SubmitVerification::KeySentAfterVisibleToken { key } => {
752
763
  format!("{}_sent_after_visible_token", tmux_key_name(key))
753
764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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.2",
24
- "@team-agent/cli-darwin-x64": "0.3.2",
25
- "@team-agent/cli-linux-x64": "0.3.2"
23
+ "@team-agent/cli-darwin-arm64": "0.3.4",
24
+ "@team-agent/cli-darwin-x64": "0.3.4",
25
+ "@team-agent/cli-linux-x64": "0.3.4"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",