@team-agent/installer 0.3.0 → 0.3.2

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 (39) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +38 -7
  4. package/crates/team-agent/src/cli/emit.rs +182 -54
  5. package/crates/team-agent/src/cli/mod.rs +703 -35
  6. package/crates/team-agent/src/cli/status_port.rs +170 -44
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  8. package/crates/team-agent/src/cli/types.rs +1 -0
  9. package/crates/team-agent/src/coordinator/health.rs +130 -0
  10. package/crates/team-agent/src/leader/lease.rs +23 -2
  11. package/crates/team-agent/src/leader/rediscover/tests.rs +1 -0
  12. package/crates/team-agent/src/leader/rediscover.rs +2 -0
  13. package/crates/team-agent/src/leader/tests/byte_findings.rs +9 -6
  14. package/crates/team-agent/src/leader/tests/idle.rs +1 -0
  15. package/crates/team-agent/src/leader/tests/lease_claim.rs +157 -0
  16. package/crates/team-agent/src/leader/types.rs +2 -0
  17. package/crates/team-agent/src/lifecycle/launch.rs +554 -65
  18. package/crates/team-agent/src/lifecycle/restart/common.rs +65 -0
  19. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +57 -15
  20. package/crates/team-agent/src/lifecycle/restart/remove.rs +5 -1
  21. package/crates/team-agent/src/lifecycle/restart.rs +20 -0
  22. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +52 -0
  23. package/crates/team-agent/src/lifecycle/types.rs +25 -0
  24. package/crates/team-agent/src/mcp_server/tests/wire.rs +28 -0
  25. package/crates/team-agent/src/mcp_server/wire.rs +81 -1
  26. package/crates/team-agent/src/messaging/delivery.rs +574 -12
  27. package/crates/team-agent/src/messaging/leader_receiver.rs +26 -37
  28. package/crates/team-agent/src/messaging/mod.rs +1 -1
  29. package/crates/team-agent/src/messaging/results.rs +218 -49
  30. package/crates/team-agent/src/messaging/send.rs +15 -19
  31. package/crates/team-agent/src/provider/adapter.rs +95 -10
  32. package/crates/team-agent/src/provider/helpers.rs +10 -1
  33. package/crates/team-agent/src/state/identity.rs +3 -0
  34. package/crates/team-agent/src/state/persist.rs +113 -1
  35. package/crates/team-agent/src/state/projection.rs +127 -3
  36. package/crates/team-agent/src/tmux_backend/tests.rs +179 -0
  37. package/crates/team-agent/src/tmux_backend.rs +124 -12
  38. package/npm/install.mjs +29 -7
  39. package/package.json +4 -4
@@ -161,7 +161,12 @@ pub struct TmuxBackend {
161
161
  runner: Box<dyn CommandRunner>,
162
162
  /// `Some(name)` for a per-team socket -> every `tmux` argv gets `-L <name>` injected after the
163
163
  /// leading "tmux" token; `None` (default) -> bare `tmux` on the shared default socket.
164
- socket: Option<String>,
164
+ socket: Option<TmuxSocketEndpoint>,
165
+ }
166
+
167
+ enum TmuxSocketEndpoint {
168
+ Name(String),
169
+ Path(String),
165
170
  }
166
171
 
167
172
  impl TmuxBackend {
@@ -177,7 +182,25 @@ impl TmuxBackend {
177
182
  pub fn for_workspace(workspace: &Path) -> Self {
178
183
  Self {
179
184
  runner: Box::new(RealCommandRunner),
180
- socket: Some(socket_name_for_workspace(workspace)),
185
+ socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))),
186
+ }
187
+ }
188
+
189
+ pub(crate) fn for_socket_name(socket: &str) -> Self {
190
+ if socket.is_empty() || socket == "default" {
191
+ Self::new()
192
+ } else {
193
+ Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Name(socket.to_string())) }
194
+ }
195
+ }
196
+
197
+ pub(crate) fn for_tmux_endpoint(endpoint: &str) -> Self {
198
+ if endpoint.is_empty() || endpoint == "default" {
199
+ Self::new()
200
+ } else if Path::new(endpoint).is_absolute() {
201
+ Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
202
+ } else {
203
+ Self::new()
181
204
  }
182
205
  }
183
206
 
@@ -189,7 +212,17 @@ impl TmuxBackend {
189
212
  /// Backend with an injected runner bound to a per-workspace socket (tests: assert the `-L` is in
190
213
  /// the recorded argv for a workspace-bound backend).
191
214
  pub fn with_runner_for_workspace(runner: Box<dyn CommandRunner>, workspace: &Path) -> Self {
192
- Self { runner, socket: Some(socket_name_for_workspace(workspace)) }
215
+ Self { runner, socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))) }
216
+ }
217
+
218
+ pub(crate) fn with_runner_for_tmux_endpoint(runner: Box<dyn CommandRunner>, endpoint: &str) -> Self {
219
+ if Path::new(endpoint).is_absolute() {
220
+ Self { runner, socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
221
+ } else if endpoint.is_empty() || endpoint == "default" {
222
+ Self { runner, socket: None }
223
+ } else {
224
+ Self { runner, socket: None }
225
+ }
193
226
  }
194
227
 
195
228
  /// THE RUN CHOKEPOINT: every executed `tmux` argv is funneled through here. When a per-team
@@ -197,11 +230,19 @@ impl TmuxBackend {
197
230
  /// through unchanged. Non-`tmux` argv (e.g. the spawned provider command) is never rewritten.
198
231
  fn tmux_argv(&self, argv: &[String]) -> Vec<String> {
199
232
  match &self.socket {
200
- Some(socket) if argv.first().map(String::as_str) == Some("tmux") => {
233
+ Some(endpoint) if argv.first().map(String::as_str) == Some("tmux") => {
201
234
  let mut out = Vec::with_capacity(argv.len() + 2);
202
235
  out.push("tmux".to_string());
203
- out.push("-L".to_string());
204
- out.push(socket.clone());
236
+ match endpoint {
237
+ TmuxSocketEndpoint::Name(socket) => {
238
+ out.push("-L".to_string());
239
+ out.push(socket.clone());
240
+ }
241
+ TmuxSocketEndpoint::Path(socket) => {
242
+ out.push("-S".to_string());
243
+ out.push(socket.clone());
244
+ }
245
+ }
205
246
  out.extend(argv.iter().skip(1).cloned());
206
247
  out
207
248
  }
@@ -237,6 +278,17 @@ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
237
278
  format!("ta-{:012x}", hasher.finish() & 0xffff_ffff_ffff)
238
279
  }
239
280
 
281
+ pub(crate) fn socket_name_from_tmux_env() -> Option<String> {
282
+ let tmux = std::env::var("TMUX")
283
+ .ok()
284
+ .filter(|value| !value.is_empty())?;
285
+ let socket_path = tmux.split(',').next().unwrap_or("").trim();
286
+ if socket_path.is_empty() || !Path::new(socket_path).is_absolute() {
287
+ return None;
288
+ }
289
+ Some(socket_path.to_string())
290
+ }
291
+
240
292
  /// Deterministic FNV-1a (64-bit) — std `DefaultHasher` is NOT stable across releases, so a fixed
241
293
  /// FNV keeps the socket identical for the CLI, the daemon, and every later op on the same workspace.
242
294
  struct Fnv1a(u64);
@@ -442,6 +494,14 @@ fn submit_verification_for_key(key: Key) -> SubmitVerification {
442
494
  }
443
495
  }
444
496
 
497
+ fn capture_has_pasted_content_prompt(text: &str) -> bool {
498
+ let lower = text.to_ascii_lowercase();
499
+ lower.contains("pasted content") || lower.contains("pasted text")
500
+ }
501
+
502
+ const PASTED_CONTENT_APPEAR_POLLS: u32 = 5;
503
+ const PASTED_CONTENT_SUBMIT_ATTEMPTS: u32 = 3;
504
+
445
505
  fn shell_command(argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> String {
446
506
  let mut parts = Vec::new();
447
507
  parts.push("cd".to_string());
@@ -527,7 +587,40 @@ impl Transport for TmuxBackend {
527
587
  self.run_inject_stage(&argv, stage)?;
528
588
  }
529
589
  }
590
+ let mut saw_pasted_prompt = false;
591
+ for _ in 0..PASTED_CONTENT_APPEAR_POLLS {
592
+ let captured = self.capture(target, CaptureRange::Tail(80))?;
593
+ if capture_has_pasted_content_prompt(&captured.text) {
594
+ saw_pasted_prompt = true;
595
+ break;
596
+ }
597
+ std::thread::sleep(Duration::from_millis(25));
598
+ }
530
599
  let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
600
+ if saw_pasted_prompt {
601
+ let mut attempts = 0;
602
+ let mut cleared = false;
603
+ for _ in 0..PASTED_CONTENT_SUBMIT_ATTEMPTS {
604
+ attempts += 1;
605
+ self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
606
+ let captured = self.capture(target, CaptureRange::Tail(80))?;
607
+ if !capture_has_pasted_content_prompt(&captured.text) {
608
+ cleared = true;
609
+ break;
610
+ }
611
+ }
612
+ return Ok(InjectReport {
613
+ stage_reached: InjectStage::Submit,
614
+ inject_verification: InjectVerification::CaptureContainsNewPastedContentPrompt,
615
+ submit_verification: if cleared {
616
+ SubmitVerification::PastedContentPromptAbsentAfterSubmit
617
+ } else {
618
+ submit_verification_for_key(submit)
619
+ },
620
+ turn_verification: TurnVerification::NotYetObserved,
621
+ attempts,
622
+ });
623
+ }
531
624
  self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
532
625
  }
533
626
  }
@@ -622,7 +715,10 @@ impl Transport for TmuxBackend {
622
715
  }
623
716
  let mut panes = Vec::new();
624
717
  for line in output.stdout.lines().filter(|line| !line.is_empty()) {
625
- if let Some(pane) = parse_pane_info_line(line) {
718
+ if let Some(mut pane) = parse_pane_info_line(line) {
719
+ if pane.pane_pid.is_none() {
720
+ pane.pane_pid = query_pane_pid(self, &pane.pane_id)?;
721
+ }
626
722
  panes.push(pane);
627
723
  }
628
724
  }
@@ -685,22 +781,22 @@ impl Transport for TmuxBackend {
685
781
  }
686
782
 
687
783
  fn kill_session(&self, session: &SessionName) -> Result<(), TransportError> {
688
- let argv = vec![
784
+ let argv = self.tmux_argv(&[
689
785
  "tmux".to_string(),
690
786
  "kill-session".to_string(),
691
787
  "-t".to_string(),
692
788
  session.as_str().to_string(),
693
- ];
789
+ ]);
694
790
  self.run_ok(&argv)
695
791
  }
696
792
 
697
793
  fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
698
- let argv = vec![
794
+ let argv = self.tmux_argv(&[
699
795
  "tmux".to_string(),
700
796
  "kill-window".to_string(),
701
797
  "-t".to_string(),
702
798
  target_name(target),
703
- ];
799
+ ]);
704
800
  self.run_ok(&argv)
705
801
  }
706
802
 
@@ -719,6 +815,22 @@ impl Transport for TmuxBackend {
719
815
  }
720
816
  }
721
817
 
818
+ fn query_pane_pid(backend: &TmuxBackend, pane: &PaneId) -> Result<Option<u32>, TransportError> {
819
+ let argv = backend.tmux_argv(&[
820
+ "tmux".to_string(),
821
+ "display-message".to_string(),
822
+ "-p".to_string(),
823
+ "-t".to_string(),
824
+ pane.as_str().to_string(),
825
+ "#{pane_pid}".to_string(),
826
+ ]);
827
+ let output = backend.runner.run(&argv)?;
828
+ if !output.success {
829
+ return Ok(None);
830
+ }
831
+ Ok(parse_optional_u32(output.stdout.trim()))
832
+ }
833
+
722
834
  fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
723
835
  let fields = line.split('\t').collect::<Vec<_>>();
724
836
  if fields.len() < 11 {
@@ -734,7 +846,7 @@ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
734
846
  current_command: non_empty(fields[6]).map(str::to_string),
735
847
  active: fields[7] == "1",
736
848
  current_path: non_empty(fields[8]).map(PathBuf::from),
737
- pane_pid: None,
849
+ pane_pid: fields.get(11).and_then(|raw| parse_optional_u32(raw)),
738
850
  leader_env: BTreeMap::new(),
739
851
  })
740
852
  }
package/npm/install.mjs CHANGED
@@ -10,6 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const packageRoot = path.resolve(__dirname, "..");
11
11
  const require = createRequire(import.meta.url);
12
12
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
13
+ const DOCTOR_TIMEOUT_MS = 5000;
13
14
 
14
15
  const command = process.argv[2] || "install";
15
16
  const args = process.argv.slice(3);
@@ -87,11 +88,20 @@ function install(argv) {
87
88
  console.log("skill: installed for Codex and Claude");
88
89
  console.log(`PATH: ensure ${binDir} is on PATH`);
89
90
 
90
- const doctor = spawnSync(teamAgent, ["doctor", "--json"], { text: true, encoding: "utf8" });
91
- if (doctor.status === 0) {
92
- console.log("doctor: ok");
93
- } else {
94
- console.log("doctor: has blockers; run `team-agent doctor` after updating PATH");
91
+ const doctorWorkspace = makeDoctorWorkspace();
92
+ try {
93
+ const doctor = spawnSync(teamAgent, ["doctor", "--json", "--workspace", doctorWorkspace], {
94
+ text: true,
95
+ encoding: "utf8",
96
+ timeout: DOCTOR_TIMEOUT_MS,
97
+ });
98
+ if (doctor.status === 0) {
99
+ console.log("doctor: ok");
100
+ } else {
101
+ console.log("doctor: has blockers; run `team-agent doctor` after updating PATH");
102
+ }
103
+ } finally {
104
+ fs.rmSync(doctorWorkspace, { recursive: true, force: true });
95
105
  }
96
106
  }
97
107
 
@@ -103,8 +113,16 @@ function runDoctor(argv) {
103
113
  console.error(`team-agent wrapper not found: ${teamAgent}`);
104
114
  process.exit(1);
105
115
  }
106
- const proc = spawnSync(teamAgent, ["doctor"], { stdio: "inherit" });
107
- process.exit(proc.status ?? 1);
116
+ const doctorWorkspace = makeDoctorWorkspace();
117
+ try {
118
+ const proc = spawnSync(teamAgent, ["doctor", "--workspace", doctorWorkspace], {
119
+ stdio: "inherit",
120
+ timeout: DOCTOR_TIMEOUT_MS,
121
+ });
122
+ process.exit(proc.status ?? 1);
123
+ } finally {
124
+ fs.rmSync(doctorWorkspace, { recursive: true, force: true });
125
+ }
108
126
  }
109
127
 
110
128
  function uninstall(argv) {
@@ -217,6 +235,10 @@ function skillDestinations() {
217
235
  ];
218
236
  }
219
237
 
238
+ function makeDoctorWorkspace() {
239
+ return fs.mkdtempSync(path.join(os.tmpdir(), "team-agent-doctor-"));
240
+ }
241
+
220
242
  function copyTree(src, dest) {
221
243
  const stat = fs.lstatSync(src);
222
244
  if (stat.isDirectory()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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.0",
24
- "@team-agent/cli-darwin-x64": "0.3.0",
25
- "@team-agent/cli-linux-x64": "0.3.0"
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"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",