@team-agent/installer 0.3.2 → 0.3.3

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 (78) 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 +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +286 -52
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +799 -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.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +89 -20
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +818 -116
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +177 -83
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +443 -9
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +153 -16
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +483 -67
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  69. package/crates/team-agent/src/provider/types.rs +47 -0
  70. package/crates/team-agent/src/session_capture.rs +616 -0
  71. package/crates/team-agent/src/state/persist.rs +57 -0
  72. package/crates/team-agent/src/state/projection.rs +32 -23
  73. package/crates/team-agent/src/state/selector.rs +5 -2
  74. package/crates/team-agent/src/tmux_backend.rs +97 -60
  75. package/crates/team-agent/src/transport/test_support.rs +9 -0
  76. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  77. package/crates/team-agent/src/transport.rs +13 -2
  78. 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
  }
@@ -362,10 +402,13 @@ impl TmuxBackend {
362
402
 
363
403
  fn run_spawn(&self, argv: &[String]) -> Result<CommandOutput, TransportError> {
364
404
  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
- })?;
405
+ let output = self
406
+ .runner
407
+ .run(&argv)
408
+ .map_err(|source| TransportError::Spawn {
409
+ backend: BackendKind::Tmux,
410
+ source,
411
+ })?;
369
412
  if output.success {
370
413
  Ok(output)
371
414
  } else {
@@ -373,16 +416,12 @@ impl TmuxBackend {
373
416
  }
374
417
  }
375
418
 
376
- fn run_inject_stage(
377
- &self,
378
- argv: &[String],
379
- stage: InjectStage,
380
- ) -> Result<(), TransportError> {
419
+ fn run_inject_stage(&self, argv: &[String], stage: InjectStage) -> Result<(), TransportError> {
381
420
  let argv = self.tmux_argv(argv);
382
- let output = self.runner.run(&argv).map_err(|source| TransportError::Inject {
383
- stage,
384
- source,
385
- })?;
421
+ let output = self
422
+ .runner
423
+ .run(&argv)
424
+ .map_err(|source| TransportError::Inject { stage, source })?;
386
425
  if output.success {
387
426
  Ok(())
388
427
  } else {
@@ -397,10 +436,10 @@ impl TmuxBackend {
397
436
  stdin: &str,
398
437
  ) -> Result<(), TransportError> {
399
438
  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
- })?;
439
+ let output = self
440
+ .runner
441
+ .run_with_stdin(&argv, stdin)
442
+ .map_err(|source| TransportError::Inject { stage, source })?;
404
443
  if output.success {
405
444
  Ok(())
406
445
  } else {
@@ -611,11 +650,12 @@ impl Transport for TmuxBackend {
611
650
  }
612
651
  return Ok(InjectReport {
613
652
  stage_reached: InjectStage::Submit,
614
- inject_verification: InjectVerification::CaptureContainsNewPastedContentPrompt,
653
+ inject_verification:
654
+ InjectVerification::CaptureContainsNewPastedContentPrompt,
615
655
  submit_verification: if cleared {
616
656
  SubmitVerification::PastedContentPromptAbsentAfterSubmit
617
657
  } else {
618
- submit_verification_for_key(submit)
658
+ SubmitVerification::PastedContentPromptStillPresentAfterSubmit
619
659
  },
620
660
  turn_verification: TurnVerification::NotYetObserved,
621
661
  attempts,
@@ -656,7 +696,10 @@ impl Transport for TmuxBackend {
656
696
  ) -> Result<CapturedText, TransportError> {
657
697
  let pane = pane_from_target(target);
658
698
  let argv = self.tmux_argv(&tmux_capture_argv(&pane, range));
659
- let output = self.runner.run(&argv).map_err(|source| TransportError::Capture { source })?;
699
+ let output = self
700
+ .runner
701
+ .run(&argv)
702
+ .map_err(|source| TransportError::Capture { source })?;
660
703
  if !output.success {
661
704
  return Err(subprocess_error(argv, output));
662
705
  }
@@ -666,11 +709,7 @@ impl Transport for TmuxBackend {
666
709
  })
667
710
  }
668
711
 
669
- fn query(
670
- &self,
671
- target: &Target,
672
- field: PaneField,
673
- ) -> Result<Option<String>, TransportError> {
712
+ fn query(&self, target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
674
713
  let pane = pane_from_target(target);
675
714
  let argv = self.tmux_argv(&tmux_query_argv(&pane, field));
676
715
  let output = self.runner.run(&argv)?;
@@ -693,7 +732,11 @@ impl Transport for TmuxBackend {
693
732
  if output.success {
694
733
  return Ok(PaneLiveness::Live);
695
734
  }
696
- if output.stderr.to_ascii_lowercase().contains("can't find pane") {
735
+ if output
736
+ .stderr
737
+ .to_ascii_lowercase()
738
+ .contains("can't find pane")
739
+ {
697
740
  Ok(PaneLiveness::Dead)
698
741
  } else {
699
742
  Ok(PaneLiveness::Unknown)
@@ -736,10 +779,7 @@ impl Transport for TmuxBackend {
736
779
  Ok(output.success)
737
780
  }
738
781
 
739
- fn list_windows(
740
- &self,
741
- session: &SessionName,
742
- ) -> Result<Vec<WindowName>, TransportError> {
782
+ fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
743
783
  // golden runtime.py:1023-1029 `_tmux_window_exists`: `tmux list-windows -t <s> -F #{window_name}`;
744
784
  // returncode != 0 -> false (here: an empty window set), else the window names by line.
745
785
  let argv = self.tmux_argv(&[
@@ -800,10 +840,7 @@ impl Transport for TmuxBackend {
800
840
  self.run_ok(&argv)
801
841
  }
802
842
 
803
- fn attach_session(
804
- &self,
805
- session: &SessionName,
806
- ) -> Result<AttachOutcome, TransportError> {
843
+ fn attach_session(&self, session: &SessionName) -> Result<AttachOutcome, TransportError> {
807
844
  let argv = [
808
845
  "tmux".to_string(),
809
846
  "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.3",
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.3",
24
+ "@team-agent/cli-darwin-x64": "0.3.3",
25
+ "@team-agent/cli-linux-x64": "0.3.3"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",