@team-agent/installer 0.5.113 → 0.5.114

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 (46) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +55 -20
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/mod.rs +152 -8
  10. package/crates/team-agent/src/cli/models.rs +68 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  17. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  18. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  19. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  20. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  21. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  22. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  23. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  24. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  25. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  26. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  27. package/crates/team-agent/src/leader/mod.rs +1 -0
  28. package/crates/team-agent/src/leader/prompt.rs +906 -0
  29. package/crates/team-agent/src/leader/start.rs +260 -16
  30. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  31. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  32. package/crates/team-agent/src/leader/types.rs +6 -0
  33. package/crates/team-agent/src/lib.rs +4 -0
  34. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
  35. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  36. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  38. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  39. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  40. package/crates/team-agent/src/main.rs +6 -0
  41. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  42. package/crates/team-agent/src/messaging/send.rs +10 -0
  43. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  44. package/crates/team-agent/src/model/spec.rs +24 -12
  45. package/crates/team-agent/src/state/persist.rs +73 -0
  46. package/package.json +4 -4
@@ -153,12 +153,8 @@ enum MatchResult<'a> {
153
153
 
154
154
  /// The CLI's concise JSON projection. `agent` filters the registered list but
155
155
  /// never synthesizes an unregistered node.
156
- pub(crate) fn status_brief_scoped(
157
- _workspace: &Path,
158
- state: &Value,
159
- agent: Option<&str>,
160
- ) -> Value {
161
- let nodes = project_status_nodes(state, agent);
156
+ pub(crate) fn status_brief_scoped(workspace: &Path, state: &Value, agent: Option<&str>) -> Value {
157
+ let nodes = project_status_nodes(workspace, state, agent);
162
158
  json!({
163
159
  "nodes": nodes
164
160
  .into_iter()
@@ -169,12 +165,8 @@ pub(crate) fn status_brief_scoped(
169
165
 
170
166
  /// Human output keeps the legacy single-node form and groups shared attach
171
167
  /// context only when multiple nodes make that repetition useful.
172
- pub(crate) fn format_status_brief(
173
- _workspace: &Path,
174
- state: &Value,
175
- agent: Option<&str>,
176
- ) -> String {
177
- let nodes = project_status_nodes(state, agent);
168
+ pub(crate) fn format_status_brief(workspace: &Path, state: &Value, agent: Option<&str>) -> String {
169
+ let nodes = project_status_nodes(workspace, state, agent);
178
170
  if nodes.len() <= 1 {
179
171
  return nodes
180
172
  .into_iter()
@@ -186,14 +178,47 @@ pub(crate) fn format_status_brief(
186
178
  }
187
179
 
188
180
  fn project_status_nodes(
181
+ _workspace: &Path,
189
182
  state: &Value,
190
183
  agent: Option<&str>,
191
184
  ) -> Vec<(BriefNode, Option<AttachContext>)> {
192
185
  let registered = registered_nodes(state, agent);
193
- let probes = probe_registered_endpoints(&registered);
186
+ let legacy: Vec<_> = registered
187
+ .iter()
188
+ .filter(|_node| {
189
+ #[cfg(unix)]
190
+ if crate::contract_runtime::registry::descriptor(&_node.provider).is_some() {
191
+ return false;
192
+ }
193
+ true
194
+ })
195
+ .cloned()
196
+ .collect();
197
+ let probes = probe_registered_endpoints(&legacy);
194
198
  registered
195
199
  .iter()
196
200
  .map(|node| {
201
+ #[cfg(unix)]
202
+ if crate::contract_runtime::registry::descriptor(&node.provider).is_some() {
203
+ let mut brief = BriefNode::unknown(node);
204
+ if let Ok(native) = crate::contract_runtime::framework::project_seat(
205
+ _workspace,
206
+ &crate::state::projection::team_state_key(state),
207
+ &node.name,
208
+ ) {
209
+ brief.runtime_status = native["status"].as_str().unwrap_or("unknown").into();
210
+ brief.model = native["model"].as_str().map(str::to_owned);
211
+ brief.effort = native["effort"].as_str().map(str::to_owned);
212
+ if brief.runtime_status == "running" {
213
+ brief.tmux_command = native["native_endpoint"].as_str().map(|endpoint| {
214
+ format!("tmux -S {} attach-session", shell_quote(endpoint))
215
+ });
216
+ }
217
+ }
218
+ // Process alive is T1 only: activity/health remain unknown until
219
+ // independently observed. Never probe a private %0 on old tmux.
220
+ return (brief, None);
221
+ }
197
222
  project_node_with_attach(node, probes.get(node.endpoint.as_deref().unwrap_or("")))
198
223
  })
199
224
  .collect()
@@ -276,10 +301,7 @@ pub(crate) fn with_test_nodeprobe_resolver_delay<R>(
276
301
  }
277
302
 
278
303
  #[cfg(test)]
279
- fn with_test_nodeprobe_candidate<R>(
280
- paths: Vec<PathBuf>,
281
- func: impl FnOnce() -> R,
282
- ) -> R {
304
+ fn with_test_nodeprobe_candidate<R>(paths: Vec<PathBuf>, func: impl FnOnce() -> R) -> R {
283
305
  struct Guard(Option<Vec<PathBuf>>);
284
306
  impl Drop for Guard {
285
307
  fn drop(&mut self) {
@@ -322,8 +344,11 @@ fn write_test_capability_receipt(path: &Path) {
322
344
  "capabilities": NODEPROBE_CAPABILITIES,
323
345
  "forbidden": NODEPROBE_FORBIDDEN,
324
346
  });
325
- std::fs::write(nodeprobe_receipt_path(path), serde_json::to_vec(&receipt).unwrap())
326
- .expect("write test nodeprobe capability receipt");
347
+ std::fs::write(
348
+ nodeprobe_receipt_path(path),
349
+ serde_json::to_vec(&receipt).unwrap(),
350
+ )
351
+ .expect("write test nodeprobe capability receipt");
327
352
  }
328
353
 
329
354
  fn format_brief_node(value: &Value) -> Option<String> {
@@ -345,9 +370,10 @@ fn format_multi_node_brief(nodes: Vec<(BriefNode, Option<AttachContext>)>) -> St
345
370
  let mut groups = Vec::<(String, String)>::new();
346
371
  for (_, context) in &nodes {
347
372
  let Some(context) = context else { continue };
348
- if !groups.iter().any(|(endpoint, session)| {
349
- endpoint == &context.endpoint && session == &context.session
350
- }) {
373
+ if !groups
374
+ .iter()
375
+ .any(|(endpoint, session)| endpoint == &context.endpoint && session == &context.session)
376
+ {
351
377
  groups.push((context.endpoint.clone(), context.session.clone()));
352
378
  }
353
379
  }
@@ -355,9 +381,7 @@ fn format_multi_node_brief(nodes: Vec<(BriefNode, Option<AttachContext>)>) -> St
355
381
  let mut lines = groups
356
382
  .iter()
357
383
  .enumerate()
358
- .map(|(index, (endpoint, session))| {
359
- format_attach_template(index + 1, endpoint, session)
360
- })
384
+ .map(|(index, (endpoint, session))| format_attach_template(index + 1, endpoint, session))
361
385
  .collect::<Vec<_>>();
362
386
  if !groups.is_empty() {
363
387
  lines.push(
@@ -543,12 +567,14 @@ fn probe_registered_endpoints(nodes: &[RegisteredNode]) -> BTreeMap<String, Opti
543
567
  endpoints
544
568
  }
545
569
 
546
- fn sample_native_endpoint(
547
- endpoint: &str,
548
- registered: &[RegisteredNode],
549
- ) -> Option<ProbeResult> {
570
+ fn sample_native_endpoint(endpoint: &str, registered: &[RegisteredNode]) -> Option<ProbeResult> {
550
571
  let output = run_native_tmux_list_panes(endpoint)?;
551
- Some(sample_native_panes(&output, endpoint, registered, process_snapshots))
572
+ Some(sample_native_panes(
573
+ &output,
574
+ endpoint,
575
+ registered,
576
+ process_snapshots,
577
+ ))
552
578
  }
553
579
 
554
580
  fn sample_native_panes(
@@ -557,10 +583,21 @@ fn sample_native_panes(
557
583
  registered: &[RegisteredNode],
558
584
  snapshots: impl FnOnce(&BTreeSet<u32>) -> BTreeMap<u32, ProcessSnapshot>,
559
585
  ) -> ProbeResult {
560
- let panes = output.lines().filter_map(native_pane_fields).collect::<Vec<_>>();
561
- let pids = panes.iter().filter_map(|fields| native_pane_pid(fields[3])).collect::<BTreeSet<_>>();
562
- let processes = if pids.is_empty() { BTreeMap::new() } else { snapshots(&pids) };
563
- let nodes = panes.into_iter()
586
+ let panes = output
587
+ .lines()
588
+ .filter_map(native_pane_fields)
589
+ .collect::<Vec<_>>();
590
+ let pids = panes
591
+ .iter()
592
+ .filter_map(|fields| native_pane_pid(fields[3]))
593
+ .collect::<BTreeSet<_>>();
594
+ let processes = if pids.is_empty() {
595
+ BTreeMap::new()
596
+ } else {
597
+ snapshots(&pids)
598
+ };
599
+ let nodes = panes
600
+ .into_iter()
564
601
  .filter_map(|fields| parse_native_pane(fields, endpoint, registered, &processes))
565
602
  .collect();
566
603
  ProbeResult { nodes }
@@ -573,24 +610,36 @@ fn native_pane_fields(line: &str) -> Option<[&str; 5]> {
573
610
  let pane = fields.next()?;
574
611
  let pid = fields.next().unwrap_or_default();
575
612
  let command = fields.next().unwrap_or_default();
576
- if session.is_empty() || window.is_empty() || pane.is_empty() { return None; }
613
+ if session.is_empty() || window.is_empty() || pane.is_empty() {
614
+ return None;
615
+ }
577
616
  Some([session, window, pane, pid, command])
578
617
  }
579
618
 
580
619
  fn native_pane_pid(pid: &str) -> Option<u32> {
581
- pid.parse::<u32>().ok().filter(|pid| *pid > 0 && *pid <= i32::MAX as u32)
620
+ pid.parse::<u32>()
621
+ .ok()
622
+ .filter(|pid| *pid > 0 && *pid <= i32::MAX as u32)
582
623
  }
583
624
 
584
625
  fn run_native_tmux_list_panes(endpoint: &str) -> Option<String> {
585
- const FORMAT: &str = "#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}";
586
- let flag = if Path::new(endpoint).is_absolute() { "-S" } else { "-L" };
626
+ const FORMAT: &str =
627
+ "#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}";
628
+ let flag = if Path::new(endpoint).is_absolute() {
629
+ "-S"
630
+ } else {
631
+ "-L"
632
+ };
587
633
  let output = Command::new("tmux")
588
634
  .arg(flag)
589
635
  .arg(endpoint)
590
636
  .args(["list-panes", "-a", "-F", FORMAT])
591
637
  .output()
592
638
  .ok()?;
593
- output.status.success().then(|| String::from_utf8(output.stdout).ok())?
639
+ output
640
+ .status
641
+ .success()
642
+ .then(|| String::from_utf8(output.stdout).ok())?
594
643
  }
595
644
 
596
645
  fn parse_native_pane(
@@ -643,30 +692,52 @@ struct ProcessSnapshot {
643
692
  }
644
693
 
645
694
  fn process_snapshot_command(pids: &BTreeSet<u32>) -> Command {
646
- let pids = pids.iter().map(u32::to_string).collect::<Vec<_>>().join(",");
695
+ let pids = pids
696
+ .iter()
697
+ .map(u32::to_string)
698
+ .collect::<Vec<_>>()
699
+ .join(",");
647
700
  let mut command = Command::new("ps");
648
701
  command.args(["-o", "pid=,ppid=,stat=,comm=", "-p", &pids]);
649
702
  command
650
703
  }
651
704
 
652
705
  fn process_snapshots(pids: &BTreeSet<u32>) -> BTreeMap<u32, ProcessSnapshot> {
653
- if pids.is_empty() { return BTreeMap::new(); }
654
- let Ok(output) = process_snapshot_command(pids).output() else { return BTreeMap::new(); };
655
- if !output.status.success() { return BTreeMap::new(); }
706
+ if pids.is_empty() {
707
+ return BTreeMap::new();
708
+ }
709
+ let Ok(output) = process_snapshot_command(pids).output() else {
710
+ return BTreeMap::new();
711
+ };
712
+ if !output.status.success() {
713
+ return BTreeMap::new();
714
+ }
656
715
  parse_process_snapshots(&output.stdout, pids)
657
716
  }
658
717
 
659
- fn parse_process_snapshots(bytes: &[u8], requested: &BTreeSet<u32>) -> BTreeMap<u32, ProcessSnapshot> {
718
+ fn parse_process_snapshots(
719
+ bytes: &[u8],
720
+ requested: &BTreeSet<u32>,
721
+ ) -> BTreeMap<u32, ProcessSnapshot> {
660
722
  let mut snapshots = BTreeMap::<u32, ProcessSnapshot>::new();
661
723
  let output = String::from_utf8_lossy(bytes);
662
724
  for line in output.lines() {
663
725
  let mut fields = line.split_whitespace();
664
- let Some(pid) = fields.next().and_then(native_pane_pid) else { continue; };
665
- if !requested.contains(&pid) { continue; }
666
- let Some(_ppid) = fields.next() else { continue; };
667
- let Some(stat) = fields.next() else { continue; };
726
+ let Some(pid) = fields.next().and_then(native_pane_pid) else {
727
+ continue;
728
+ };
729
+ if !requested.contains(&pid) {
730
+ continue;
731
+ }
732
+ let Some(_ppid) = fields.next() else {
733
+ continue;
734
+ };
735
+ let Some(stat) = fields.next() else {
736
+ continue;
737
+ };
668
738
  let zombie = stat.contains('Z');
669
- snapshots.entry(pid)
739
+ snapshots
740
+ .entry(pid)
670
741
  .and_modify(|snapshot| snapshot.zombie |= zombie)
671
742
  .or_insert(ProcessSnapshot { zombie });
672
743
  }
@@ -688,11 +759,8 @@ fn probe_registered_endpoints_with_test_nodeprobe(
688
759
  continue;
689
760
  }
690
761
  if !resolved {
691
- selected_binary = resolve_nodeprobe_binary(
692
- candidates.clone(),
693
- deadline,
694
- resolver_options.clone(),
695
- );
762
+ selected_binary =
763
+ resolve_nodeprobe_binary(candidates.clone(), deadline, resolver_options.clone());
696
764
  resolved = true;
697
765
  }
698
766
  let sampled = selected_binary.as_deref().and_then(|binary| {
@@ -868,9 +936,7 @@ fn resolve_nodeprobe_binary(
868
936
  ) -> Option<PathBuf> {
869
937
  #[cfg(not(test))]
870
938
  let _ = options;
871
- if Instant::now() >= deadline
872
- || NODEPROBE_RESOLVER_ACTIVE.swap(true, Ordering::AcqRel)
873
- {
939
+ if Instant::now() >= deadline || NODEPROBE_RESOLVER_ACTIVE.swap(true, Ordering::AcqRel) {
874
940
  return None;
875
941
  }
876
942
  let (tx, rx) = mpsc::sync_channel(1);
@@ -910,10 +976,7 @@ fn resolve_nodeprobe_binary(
910
976
  }
911
977
 
912
978
  #[cfg(test)]
913
- fn select_nodeprobe_binary(
914
- candidates: Vec<PathBuf>,
915
- deadline: Instant,
916
- ) -> Option<PathBuf> {
979
+ fn select_nodeprobe_binary(candidates: Vec<PathBuf>, deadline: Instant) -> Option<PathBuf> {
917
980
  for candidate in candidates {
918
981
  if Instant::now() >= deadline {
919
982
  return None;
@@ -961,13 +1024,12 @@ fn validate_nodeprobe_candidate(path: &Path, deadline: Instant) -> Option<PathBu
961
1024
  if !receipt_metadata.is_file() || receipt_metadata.len() > NODEPROBE_RECEIPT_LIMIT {
962
1025
  return None;
963
1026
  }
964
- let receipt: NodeprobeCapabilityReceipt =
965
- serde_json::from_slice(&read_bounded_file(
966
- &receipt_path,
967
- NODEPROBE_RECEIPT_LIMIT,
968
- deadline,
969
- )?)
970
- .ok()?;
1027
+ let receipt: NodeprobeCapabilityReceipt = serde_json::from_slice(&read_bounded_file(
1028
+ &receipt_path,
1029
+ NODEPROBE_RECEIPT_LIMIT,
1030
+ deadline,
1031
+ )?)
1032
+ .ok()?;
971
1033
  if !valid_nodeprobe_receipt(&receipt, path) || Instant::now() >= deadline {
972
1034
  return None;
973
1035
  }
@@ -1227,10 +1289,7 @@ fn project_node_with_attach(
1227
1289
  let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
1228
1290
  && observed.evidence_method.as_deref() == Some("pi_activity_channel");
1229
1291
  let native_tmux = observed.evidence_method.as_deref() == Some("native_tmux");
1230
- let activity = if observed.provider.eq_ignore_ascii_case("pi")
1231
- && !pi_channel
1232
- && !native_tmux
1233
- {
1292
+ let activity = if observed.provider.eq_ignore_ascii_case("pi") && !pi_channel && !native_tmux {
1234
1293
  "unknown"
1235
1294
  } else {
1236
1295
  valid_activity(&observed.activity)
@@ -1391,9 +1450,15 @@ mod native_ps_batch_regressions {
1391
1450
 
1392
1451
  #[test]
1393
1452
  fn eight_panes_use_one_endpoint_query() {
1394
- let registered = (0..8).map(|i| registered("e1", &format!("%{i}"))).collect::<Vec<_>>();
1395
- let panes = (0..8).map(|i| format!("hp\tw\t%{i}\t{}\tpi\n", 100 + i)).collect::<String>();
1396
- let rows = (0..8).map(|i| format!("{} 1 S pi\n", 100 + i)).collect::<String>();
1453
+ let registered = (0..8)
1454
+ .map(|i| registered("e1", &format!("%{i}")))
1455
+ .collect::<Vec<_>>();
1456
+ let panes = (0..8)
1457
+ .map(|i| format!("hp\tw\t%{i}\t{}\tpi\n", 100 + i))
1458
+ .collect::<String>();
1459
+ let rows = (0..8)
1460
+ .map(|i| format!("{} 1 S pi\n", 100 + i))
1461
+ .collect::<String>();
1397
1462
  let mut calls = Vec::new();
1398
1463
  let result = sample_native_panes(&panes, "e1", &registered, |pids| {
1399
1464
  calls.push(pids.iter().copied().collect::<Vec<_>>());
@@ -1401,23 +1466,33 @@ mod native_ps_batch_regressions {
1401
1466
  });
1402
1467
  assert_eq!(calls, vec![(100..108).collect::<Vec<_>>()]);
1403
1468
  assert_eq!(result.nodes.len(), 8);
1404
- assert!(result.nodes.iter().all(|node| node.provider == "pi" && node.activity == "working" && node.health == "normal"));
1469
+ assert!(result.nodes.iter().all(|node| node.provider == "pi"
1470
+ && node.activity == "working"
1471
+ && node.health == "normal"));
1405
1472
  }
1406
1473
 
1407
1474
  #[test]
1408
1475
  fn pid_deduplication_and_command_preserve_the_privacy_whitelist() {
1409
1476
  let mut calls = Vec::new();
1410
- let result = sample_native_panes("hp\tw\t%1\t11\tsh\nhp\tw\t%2\t011\tsh\n", "e1", &[], |pids| {
1411
- calls.push(pids.iter().copied().collect::<Vec<_>>());
1412
- parse_process_snapshots(b"11 1 S sh\n", pids)
1413
- });
1477
+ let result = sample_native_panes(
1478
+ "hp\tw\t%1\t11\tsh\nhp\tw\t%2\t011\tsh\n",
1479
+ "e1",
1480
+ &[],
1481
+ |pids| {
1482
+ calls.push(pids.iter().copied().collect::<Vec<_>>());
1483
+ parse_process_snapshots(b"11 1 S sh\n", pids)
1484
+ },
1485
+ );
1414
1486
  assert_eq!(calls, vec![vec![11]]);
1415
1487
  assert_eq!(result.nodes.len(), 2);
1416
1488
  let pids = BTreeSet::from([22, 11]);
1417
1489
  let command = process_snapshot_command(&pids);
1418
1490
  assert_eq!(command.get_program(), "ps");
1419
1491
  assert_eq!(
1420
- command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect::<Vec<_>>(),
1492
+ command
1493
+ .get_args()
1494
+ .map(|arg| arg.to_string_lossy().into_owned())
1495
+ .collect::<Vec<_>>(),
1421
1496
  vec!["-o", "pid=,ppid=,stat=,comm=", "-p", "11,22"]
1422
1497
  );
1423
1498
  let command = process_snapshot_command(&BTreeSet::from([11]));
@@ -1431,8 +1506,14 @@ mod native_ps_batch_regressions {
1431
1506
  let requested = BTreeSet::from([pid, i32::MAX as u32]);
1432
1507
  let command = process_snapshot_command(&requested);
1433
1508
  // Absolute ps avoids unrelated PATH-fixture mutation in parallel unit tests.
1434
- let output = Command::new("/bin/ps").args(command.get_args()).output().unwrap();
1435
- assert!(output.status.success(), "partial PID selection must not poison the live row");
1509
+ let output = Command::new("/bin/ps")
1510
+ .args(command.get_args())
1511
+ .output()
1512
+ .unwrap();
1513
+ assert!(
1514
+ output.status.success(),
1515
+ "partial PID selection must not poison the live row"
1516
+ );
1436
1517
  let snapshots = parse_process_snapshots(&output.stdout, &requested);
1437
1518
  assert!(!snapshots.get(&pid).unwrap().zombie);
1438
1519
  assert!(!snapshots.contains_key(&(i32::MAX as u32)));
@@ -1457,14 +1538,26 @@ mod native_ps_batch_regressions {
1457
1538
  let result = sample_native_panes(panes, "e1", &[], |pids| {
1458
1539
  parse_process_snapshots(b"22 1 Z+ dead\n11 1 S sh\n999 1 S unrelated\n33 1\n", pids)
1459
1540
  });
1460
- assert_eq!(result.nodes.iter().map(|node| node.pane.as_str()).collect::<Vec<_>>(), vec!["%1", "%4"]);
1461
- assert!(result.nodes.iter().all(|node| node.activity == "idle" && node.health == "normal"));
1541
+ assert_eq!(
1542
+ result
1543
+ .nodes
1544
+ .iter()
1545
+ .map(|node| node.pane.as_str())
1546
+ .collect::<Vec<_>>(),
1547
+ vec!["%1", "%4"]
1548
+ );
1549
+ assert!(result
1550
+ .nodes
1551
+ .iter()
1552
+ .all(|node| node.activity == "idle" && node.health == "normal"));
1462
1553
  }
1463
1554
 
1464
1555
  #[test]
1465
1556
  fn empty_or_invalid_pid_sets_do_not_run_ps() {
1466
1557
  let panes = "hp\tw\t%1\t\tsh\nhp\tw\t%2\t0\tsh\nhp\tw\t%3\t1,2\tsh\nhp\tw\t%4\t4294967295\tsh\n\tw\t%5\t99\tsh\n";
1467
- let result = sample_native_panes(panes, "e1", &[], |_| panic!("no valid PID means no ps query"));
1558
+ let result = sample_native_panes(panes, "e1", &[], |_| {
1559
+ panic!("no valid PID means no ps query")
1560
+ });
1468
1561
  assert_eq!(result.nodes.len(), 1);
1469
1562
  assert_eq!(result.nodes[0].pane, "%1");
1470
1563
  assert_eq!(result.nodes[0].health, "normal");
@@ -1486,16 +1579,34 @@ mod native_ps_batch_regressions {
1486
1579
  assert!(first.nodes.is_empty());
1487
1580
  assert_eq!(second.nodes.len(), 1);
1488
1581
  let failed = Some(first);
1489
- assert_eq!(project_node_with_attach(&registered[0], Some(&failed)).0.health, "unknown");
1582
+ assert_eq!(
1583
+ project_node_with_attach(&registered[0], Some(&failed))
1584
+ .0
1585
+ .health,
1586
+ "unknown"
1587
+ );
1490
1588
  let live = Some(second);
1491
- assert_eq!(project_node_with_attach(&registered[1], Some(&live)).0.health, "normal");
1589
+ assert_eq!(
1590
+ project_node_with_attach(&registered[1], Some(&live))
1591
+ .0
1592
+ .health,
1593
+ "normal"
1594
+ );
1492
1595
  }
1493
1596
 
1494
1597
  #[test]
1495
1598
  fn duplicate_process_rows_cannot_hide_a_zombie() {
1496
1599
  let requested = BTreeSet::from([11]);
1497
- for rows in [b"11 1 Z dead\n11 1 S sh\n".as_slice(), b"11 1 S sh\n11 1 Z dead\n".as_slice()] {
1498
- assert!(parse_process_snapshots(rows, &requested).get(&11).unwrap().zombie);
1600
+ for rows in [
1601
+ b"11 1 Z dead\n11 1 S sh\n".as_slice(),
1602
+ b"11 1 S sh\n11 1 Z dead\n".as_slice(),
1603
+ ] {
1604
+ assert!(
1605
+ parse_process_snapshots(rows, &requested)
1606
+ .get(&11)
1607
+ .unwrap()
1608
+ .zombie
1609
+ );
1499
1610
  }
1500
1611
  assert!(parse_process_snapshots(b"not a process row\n", &requested).is_empty());
1501
1612
  }
@@ -1504,14 +1615,41 @@ mod native_ps_batch_regressions {
1504
1615
  fn json_and_human_keep_nine_fields_without_ps_details() {
1505
1616
  let registered = vec![registered("e1", "%1"), registered("e1", "%2")];
1506
1617
  let probe = Some(sample_native_panes(
1507
- "hp\tw\t%1\t11\tsh\nhp\tw\t%2\t22\tpi\n", "e1", &registered,
1508
- |pids| parse_process_snapshots(b"22 1 R PRIVATE_COMMAND_DETAIL\n11 1 S PRIVATE_COMMAND_DETAIL\n", pids),
1618
+ "hp\tw\t%1\t11\tsh\nhp\tw\t%2\t22\tpi\n",
1619
+ "e1",
1620
+ &registered,
1621
+ |pids| {
1622
+ parse_process_snapshots(
1623
+ b"22 1 R PRIVATE_COMMAND_DETAIL\n11 1 S PRIVATE_COMMAND_DETAIL\n",
1624
+ pids,
1625
+ )
1626
+ },
1509
1627
  ));
1510
- let projected = registered.iter().map(|node| project_node_with_attach(node, Some(&probe))).collect::<Vec<_>>();
1511
- let keys = BTreeSet::from(["name", "provider", "model", "effort", "runtime_status", "activity", "health", "session_name", "tmux_command"]);
1628
+ let projected = registered
1629
+ .iter()
1630
+ .map(|node| project_node_with_attach(node, Some(&probe)))
1631
+ .collect::<Vec<_>>();
1632
+ let keys = BTreeSet::from([
1633
+ "name",
1634
+ "provider",
1635
+ "model",
1636
+ "effort",
1637
+ "runtime_status",
1638
+ "activity",
1639
+ "health",
1640
+ "session_name",
1641
+ "tmux_command",
1642
+ ]);
1512
1643
  for (node, _) in &projected {
1513
1644
  let json = node.clone().into_json();
1514
- assert_eq!(json.as_object().unwrap().keys().map(String::as_str).collect::<BTreeSet<_>>(), keys);
1645
+ assert_eq!(
1646
+ json.as_object()
1647
+ .unwrap()
1648
+ .keys()
1649
+ .map(String::as_str)
1650
+ .collect::<BTreeSet<_>>(),
1651
+ keys
1652
+ );
1515
1653
  assert_eq!(json["model"], "fixture-model");
1516
1654
  assert_eq!(json["effort"], "high");
1517
1655
  assert_eq!(json["runtime_status"], "running");
@@ -1571,12 +1709,21 @@ mod tests {
1571
1709
  node.effort = settings.1.map(str::to_string);
1572
1710
  for lifecycle in [None, Some("stopped")] {
1573
1711
  node.lifecycle = lifecycle.map(str::to_string);
1574
- for sample in [None, Some(ProbeResult { nodes: vec![probe("pi_activity_channel")] })] {
1712
+ for sample in [
1713
+ None,
1714
+ Some(ProbeResult {
1715
+ nodes: vec![probe("pi_activity_channel")],
1716
+ }),
1717
+ ] {
1575
1718
  let value = project_node(&node, Some(&sample)).into_json();
1576
1719
  assert_eq!(value["model"], json!(settings.0));
1577
1720
  assert_eq!(value["effort"], json!(settings.1));
1578
1721
  let human = format_brief_node(&value).unwrap();
1579
- assert!(human.contains(&format!("model: {} effort: {}", settings.0.unwrap_or("null"), settings.1.unwrap_or("null"))));
1722
+ assert!(human.contains(&format!(
1723
+ "model: {} effort: {}",
1724
+ settings.0.unwrap_or("null"),
1725
+ settings.1.unwrap_or("null")
1726
+ )));
1580
1727
  assert_eq!(value.as_object().unwrap().len(), 9);
1581
1728
  }
1582
1729
  }
@@ -1606,7 +1753,12 @@ mod tests {
1606
1753
  fn pi_pane_title_is_unknown_not_idle() {
1607
1754
  let node = registered();
1608
1755
  let observed = probe("pane_title");
1609
- let projected = project_node(&node, Some(&Some(ProbeResult { nodes: vec![observed] })));
1756
+ let projected = project_node(
1757
+ &node,
1758
+ Some(&Some(ProbeResult {
1759
+ nodes: vec![observed],
1760
+ })),
1761
+ );
1610
1762
  assert_eq!(projected.runtime_status, "running");
1611
1763
  assert_eq!(projected.activity, "unknown");
1612
1764
  assert_eq!(projected.health, "unknown");
@@ -1616,7 +1768,12 @@ mod tests {
1616
1768
  fn exact_probe_generates_quoted_window_pane_command() {
1617
1769
  let node = registered();
1618
1770
  let observed = probe("pi_activity_channel");
1619
- let projected = project_node(&node, Some(&Some(ProbeResult { nodes: vec![observed] })));
1771
+ let projected = project_node(
1772
+ &node,
1773
+ Some(&Some(ProbeResult {
1774
+ nodes: vec![observed],
1775
+ })),
1776
+ );
1620
1777
  assert_eq!(projected.activity, "idle");
1621
1778
  assert_eq!(
1622
1779
  projected.tmux_command.as_deref(),
@@ -1639,7 +1796,12 @@ mod tests {
1639
1796
  let mut node = registered();
1640
1797
  node.lifecycle = Some("stopped".to_string());
1641
1798
  let observed = probe("pi_activity_channel");
1642
- let projected = project_node(&node, Some(&Some(ProbeResult { nodes: vec![observed] })));
1799
+ let projected = project_node(
1800
+ &node,
1801
+ Some(&Some(ProbeResult {
1802
+ nodes: vec![observed],
1803
+ })),
1804
+ );
1643
1805
  assert_eq!(projected.runtime_status, "unknown");
1644
1806
  assert!(projected.tmux_command.is_none());
1645
1807
  }
@@ -1663,7 +1825,12 @@ mod tests {
1663
1825
  let node = registered();
1664
1826
  let mut observed = probe("pi_activity_channel");
1665
1827
  observed.pane = "%9".to_string();
1666
- let projected = project_node(&node, Some(&Some(ProbeResult { nodes: vec![observed] })));
1828
+ let projected = project_node(
1829
+ &node,
1830
+ Some(&Some(ProbeResult {
1831
+ nodes: vec![observed],
1832
+ })),
1833
+ );
1667
1834
  assert_eq!(projected.runtime_status, "unknown");
1668
1835
  }
1669
1836